use anyhow::{Context, Result};
use tycho_types::cell::Lazy;
use tycho_types::dict;
use tycho_types::error::Error;
use tycho_types::models::{
    Account, AccountState, AccountStatus, BlockId, CurrencyCollection, HashUpdate, IntAddr,
    LibDescr, Message, OwnedMessage, ShardAccount, SimpleLib, StdAddr, StorageInfo, StorageUsed,
    TickTock, Transaction, TxInfo,
};
use tycho_types::num::{Tokens, Uint15};
use tycho_types::prelude::*;

pub use self::config::ParsedConfig;
pub use self::error::{TxError, TxResult};
use self::util::new_varuint56_truncate;
pub use self::util::{ExtStorageStat, OwnedExtStorageStat, StorageStatLimits};

mod config;
mod error;
mod util;

pub mod phase {
    pub use self::action::{ActionPhaseContext, ActionPhaseFull};
    pub use self::bounce::BouncePhaseContext;
    pub use self::compute::{
        ComputePhaseContext, ComputePhaseFull, ComputePhaseSmcInfo, TransactionInput,
    };
    pub use self::receive::{MsgStateInit, ReceivedMessage};
    pub use self::storage::StoragePhaseContext;

    mod action;
    mod bounce;
    mod compute;
    mod credit;
    mod receive;
    mod storage;
}

mod tx {
    mod ordinary;
    mod ticktock;
}

/// Transaction executor.
#[derive(Debug)]
pub struct Executor<'a> {
    params: &'a ExecutorParams,
    config: &'a ParsedConfig,
    min_lt: u64,
    override_special: Option<bool>,
}

impl<'a> Executor<'a> {
    pub fn new(params: &'a ExecutorParams, config: &'a ParsedConfig) -> Self {
        Self {
            params,
            config,
            min_lt: 0,
            override_special: None,
        }
    }

    pub fn with_min_lt(mut self, min_lt: u64) -> Self {
        self.set_min_lt(min_lt);
        self
    }

    pub fn set_min_lt(&mut self, min_lt: u64) {
        self.min_lt = min_lt;
    }

    pub fn override_special(mut self, is_special: bool) -> Self {
        self.override_special = Some(is_special);
        self
    }

    #[inline]
    pub fn begin_ordinary<'s, M>(
        &self,
        address: &StdAddr,
        is_external: bool,
        msg: M,
        state: &'s ShardAccount,
    ) -> TxResult<UncommittedTransaction<'a, 's>>
    where
        M: LoadMessage,
    {
        self.begin_ordinary_ext(address, is_external, msg, state, None)
    }

    pub fn begin_ordinary_ext<'s, M>(
        &self,
        address: &StdAddr,
        is_external: bool,
        msg: M,
        state: &'s ShardAccount,
        inspector: Option<&mut ExecutorInspector<'_>>,
    ) -> TxResult<UncommittedTransaction<'a, 's>>
    where
        M: LoadMessage,
    {
        let msg_root = msg.load_message_root()?;

        let account = state.load_account()?;
        let mut exec = self.begin(address, account)?;
        let info = exec.run_ordinary_transaction(is_external, msg_root.clone(), inspector)?;

        UncommittedTransaction::with_info(exec, state, Some(msg_root), info).map_err(TxError::Fatal)
    }

    #[inline]
    pub fn check_ordinary<M>(&self, address: &StdAddr, msg: M, state: &ShardAccount) -> TxResult<()>
    where
        M: LoadMessage,
    {
        self.check_ordinary_ext(address, msg, state, None)
    }

    pub fn check_ordinary_ext<M>(
        &self,
        address: &StdAddr,
        msg: M,
        state: &ShardAccount,
        inspector: Option<&mut ExecutorInspector<'_>>,
    ) -> TxResult<()>
    where
        M: LoadMessage,
    {
        let msg_root = msg.load_message_root()?;
        let account = state.load_account()?;
        let mut exec = self.begin(address, account)?;
        exec.check_ordinary_transaction(msg_root, inspector)
    }

    #[inline]
    pub fn begin_tick_tock<'s>(
        &self,
        address: &StdAddr,
        kind: TickTock,
        state: &'s ShardAccount,
    ) -> TxResult<UncommittedTransaction<'a, 's>> {
        self.begin_tick_tock_ext(address, kind, state, None)
    }

    pub fn begin_tick_tock_ext<'s>(
        &self,
        address: &StdAddr,
        kind: TickTock,
        state: &'s ShardAccount,
        inspector: Option<&mut ExecutorInspector<'_>>,
    ) -> TxResult<UncommittedTransaction<'a, 's>> {
        let account = state.load_account()?;
        let mut exec = self.begin(address, account)?;
        let info = exec.run_tick_tock_transaction(kind, inspector)?;

        UncommittedTransaction::with_info(exec, state, None, info).map_err(TxError::Fatal)
    }

    pub fn begin(&self, address: &StdAddr, account: Option<Account>) -> Result<ExecutorState<'a>> {
        let is_special = self
            .override_special
            .unwrap_or_else(|| self.config.is_special(address));

        let acc_address;
        let acc_storage_stat;
        let acc_balance;
        let acc_state;
        let orig_status;
        let end_status;
        let start_lt;
        match account {
            Some(acc) => {
                acc_address = 'addr: {
                    if let IntAddr::Std(acc_addr) = acc.address
                        && acc_addr == *address
                    {
                        break 'addr acc_addr;
                    }
                    anyhow::bail!("account address mismatch");
                };
                acc_storage_stat = acc.storage_stat;
                acc_balance = acc.balance;
                acc_state = acc.state;
                orig_status = acc_state.status();
                end_status = orig_status;
                start_lt = std::cmp::max(self.min_lt, acc.last_trans_lt);
            }
            None => {
                acc_address = address.clone();
                acc_storage_stat = StorageInfo {
                    used: StorageUsed::ZERO,
                    storage_extra: Default::default(),
                    last_paid: 0,
                    due_payment: None,
                };
                acc_balance = CurrencyCollection::ZERO;
                acc_state = AccountState::Uninit;
                orig_status = AccountStatus::NotExists;
                end_status = AccountStatus::Uninit;
                start_lt = self.min_lt;
            }
        };

        let mut is_marks_authority = false;
        let mut is_suspended_by_marks = false;
        if self.params.authority_marks_enabled
            && let Some(marks) = &self.config.authority_marks
        {
            is_marks_authority = marks.is_authority(address);
            is_suspended_by_marks =
                !is_special && !is_marks_authority && marks.is_suspended(&acc_balance)?;
        }

        Ok(ExecutorState {
            params: self.params,
            config: self.config,
            is_special,
            is_marks_authority,
            is_suspended_by_marks,
            address: acc_address,
            storage_stat: acc_storage_stat,
            balance: acc_balance,
            state: acc_state,
            orig_status,
            end_status,
            start_lt,
            end_lt: start_lt + 1,
            out_msgs: Vec::new(),
            total_fees: Tokens::ZERO,
            burned: Tokens::ZERO,
            cached_storage_stat: None,
        })
    }
}

/// Executor internals inspector.
#[derive(Default)]
pub struct ExecutorInspector<'e> {
    /// Actions list from compute phase.
    pub actions: Option<Cell>,
    /// A set of changes of the public libraries dict.
    ///
    /// NOTE: The order is the same as the actions order so
    /// it can contain duplicates and must be folded before use.
    pub public_libs_diff: Vec<PublicLibraryChange>,
    /// Compute phase exit code.
    pub exit_code: Option<i32>,
    /// Hash of the library in case it was missing during execution.
    pub missing_library: Option<HashBytes>,
    /// Total gas consumed (including the remaining "free" gas
    /// and everything that exceeds the limit).
    pub total_gas_used: u64,
    /// Debug output target.
    pub debug: Option<&'e mut dyn std::fmt::Write>,
    /// Hook for a compute phase to modify C7 register data.
    pub modify_smc_info: Option<&'e mut ModifySmcInfoFn>,
}

/// Public library diff operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PublicLibraryChange {
    Add(Cell),
    Remove(HashBytes),
}

impl PublicLibraryChange {
    /// Returns a hash of the changed library.
    pub fn lib_hash(&self) -> &HashBytes {
        match self {
            Self::Add(cell) => cell.repr_hash(),
            Self::Remove(hash) => hash,
        }
    }
}

/// Hook for a compute phase to modify C7 register data.
pub type ModifySmcInfoFn = dyn FnMut(&mut phase::ComputePhaseSmcInfo) -> Result<()>;

/// Shared state for executor phases.
#[derive(Debug)]
pub struct ExecutorState<'a> {
    pub params: &'a ExecutorParams,
    pub config: &'a ParsedConfig,

    pub is_special: bool,
    pub is_marks_authority: bool,
    pub is_suspended_by_marks: bool,

    pub address: StdAddr,
    pub storage_stat: StorageInfo,
    pub balance: CurrencyCollection,
    pub state: AccountState,

    pub orig_status: AccountStatus,
    pub end_status: AccountStatus,
    pub start_lt: u64,
    pub end_lt: u64,

    pub out_msgs: Vec<Lazy<OwnedMessage>>,
    pub total_fees: Tokens,

    pub burned: Tokens,

    pub cached_storage_stat: Option<OwnedExtStorageStat>,
}

#[cfg(test)]
impl<'a> ExecutorState<'a> {
    pub(crate) fn new_non_existent(
        params: &'a ExecutorParams,
        config: &'a impl AsRef<ParsedConfig>,
        address: &StdAddr,
    ) -> Self {
        Self {
            params,
            config: config.as_ref(),
            is_special: false,
            is_marks_authority: false,
            is_suspended_by_marks: false,
            address: address.clone(),
            storage_stat: Default::default(),
            balance: CurrencyCollection::ZERO,
            state: AccountState::Uninit,
            orig_status: AccountStatus::NotExists,
            end_status: AccountStatus::Uninit,
            start_lt: 0,
            end_lt: 1,
            out_msgs: Vec::new(),
            total_fees: Tokens::ZERO,
            burned: Tokens::ZERO,
            cached_storage_stat: None,
        }
    }

    pub(crate) fn new_uninit(
        params: &'a ExecutorParams,
        config: &'a impl AsRef<ParsedConfig>,
        address: &StdAddr,
        balance: impl Into<CurrencyCollection>,
    ) -> Self {
        let mut res = Self::new_non_existent(params, config, address);
        res.balance = balance.into();
        res.orig_status = AccountStatus::Uninit;

        if params.authority_marks_enabled
            && let Some(marks) = &config.as_ref().authority_marks
        {
            res.is_marks_authority = marks.is_authority(address);
            res.is_suspended_by_marks = !res.is_special
                && !res.is_marks_authority
                && marks.is_suspended(&res.balance).unwrap();
        }

        res
    }

    pub(crate) fn new_frozen(
        params: &'a ExecutorParams,
        config: &'a impl AsRef<ParsedConfig>,
        address: &StdAddr,
        balance: impl Into<CurrencyCollection>,
        state_hash: HashBytes,
    ) -> Self {
        let mut res = Self::new_uninit(params, config, address, balance);
        res.state = AccountState::Frozen(state_hash);
        res.orig_status = AccountStatus::Frozen;
        res.end_status = AccountStatus::Frozen;
        res
    }

    pub(crate) fn new_active(
        params: &'a ExecutorParams,
        config: &'a impl AsRef<ParsedConfig>,
        address: &StdAddr,
        balance: impl Into<CurrencyCollection>,
        data: Cell,
        code_boc: impl AsRef<[u8]>,
    ) -> Self {
        use tycho_types::models::StateInit;

        let mut res = Self::new_uninit(params, config, address, balance);
        res.state = AccountState::Active(StateInit {
            split_depth: None,
            special: None,
            code: Some(Boc::decode(code_boc).unwrap()),
            data: Some(data),
            libraries: Dict::new(),
        });
        res.orig_status = AccountStatus::Active;
        res.end_status = AccountStatus::Active;
        res
    }
}

/// Executor configuration parameters.
#[derive(Debug, Default, Clone)]
pub struct ExecutorParams {
    /// Public libraries from the referenced masterchain state.
    pub libraries: Dict<HashBytes, LibDescr>,
    /// Rand seed of the block.
    pub rand_seed: HashBytes,
    /// Unix timestamp in seconds of the block.
    pub block_unixtime: u32,
    /// Logical time of the block.
    pub block_lt: u64,
    /// Last known previous masterchain block before current.
    pub prev_mc_block_id: Option<BlockId>,
    /// VM behaviour modifiers.
    pub vm_modifiers: tycho_vm::BehaviourModifiers,
    /// Prevent [`Frozen`] accounts from being deleted
    /// when their storage due is too high.
    ///
    /// [`Frozen`]: tycho_types::models::AccountState::Frozen
    pub disable_delete_frozen_accounts: bool,
    /// Charge account balance for additional `total_action_fees`
    /// when action phase fails.
    pub charge_action_fees_on_fail: bool,
    /// Attaches an original message body as an additional cell
    /// to a bounced message body.
    pub full_body_in_bounced: bool,
    /// More gas-predictable extra currency behaviour.
    pub strict_extra_currency: bool,
    /// Whether accounts can be suspended by authority marks.
    pub authority_marks_enabled: bool,
}

/// Executed transaction.
pub struct UncommittedTransaction<'a, 's> {
    original: &'s ShardAccount,
    exec: ExecutorState<'a>,
    in_msg: Option<Cell>,
    info: Lazy<TxInfo>,
}

impl<'a, 's> UncommittedTransaction<'a, 's> {
    #[inline]
    pub fn with_info(
        exec: ExecutorState<'a>,
        original: &'s ShardAccount,
        in_msg: Option<Cell>,
        info: impl Into<TxInfo>,
    ) -> Result<Self> {
        let info = info.into();
        let info = Lazy::new(&info)?;
        Ok(Self {
            original,
            exec,
            in_msg,
            info,
        })
    }

    /// Creates a partially finalized transaction.
    ///
    /// It differs from the normal transaction by having a stub `state_update`
    /// and possibly denormalized `end_status`.
    pub fn build_uncommitted(&self) -> Result<Transaction, Error> {
        thread_local! {
            static EMPTY_STATE_UPDATE: Lazy<HashUpdate> = Lazy::new(&HashUpdate {
                old: HashBytes::ZERO,
                new: HashBytes::ZERO,
            })
            .unwrap();
        }

        self.build_transaction(self.exec.end_status, EMPTY_STATE_UPDATE.with(Clone::clone))
    }

    /// Creates a final transaction and a new contract state.
    pub fn commit(mut self) -> Result<ExecutorOutput> {
        // Collect brief account state info and build new account state.
        let account_state;
        let new_state_meta;
        let end_status = match self.build_account_state()? {
            None => {
                // TODO: Replace with a constant?
                account_state = CellBuilder::build_from(false)?;

                // Brief meta.
                new_state_meta = AccountMeta {
                    balance: CurrencyCollection::ZERO,
                    libraries: Dict::new(),
                    exists: false,
                };

                // Done
                AccountStatus::NotExists
            }
            Some(state) => {
                // Load previous account storage info.
                let prev_account_storage = 'prev: {
                    let mut cs = self.original.account.as_slice_allow_exotic();
                    if !cs.load_bit()? {
                        // account_none$0
                        break 'prev None;
                    }
                    // account$1
                    // addr:MsgAddressInt
                    IntAddr::load_from(&mut cs)?;
                    // storage_stat:StorageInfo
                    let storage_info = StorageInfo::load_from(&mut cs)?;
                    // storage:AccountStorage
                    Some((storage_info.used, cs))
                };

                // Serialize part of the new account state to compute new storage stats.
                let mut account_storage = CellBuilder::new();
                // last_trans_lt:uint64
                account_storage.store_u64(self.exec.end_lt)?;
                // balance:CurrencyCollection
                self.exec
                    .balance
                    .store_into(&mut account_storage, Cell::empty_context())?;
                // state:AccountState
                state.store_into(&mut account_storage, Cell::empty_context())?;

                // Update storage info.
                self.exec.storage_stat.used = compute_storage_used(
                    prev_account_storage,
                    account_storage.as_full_slice(),
                    &mut self.exec.cached_storage_stat,
                    self.exec.params.strict_extra_currency,
                )?;

                // Build new account state.
                account_state = CellBuilder::build_from((
                    true,                            // account$1
                    &self.exec.address,              // addr:MsgAddressInt
                    &self.exec.storage_stat,         // storage_stat:StorageInfo
                    account_storage.as_full_slice(), // storage:AccountStorage
                ))?;

                // Brief meta.
                let libraries = match &state {
                    AccountState::Active(state) => state.libraries.clone(),
                    AccountState::Frozen(..) | AccountState::Uninit => Dict::new(),
                };
                new_state_meta = AccountMeta {
                    balance: self.exec.balance.clone(),
                    libraries,
                    exists: true,
                };

                // Done
                state.status()
            }
        };

        // Serialize transaction.
        let state_update = Lazy::new(&HashUpdate {
            old: *self.original.account.repr_hash(),
            new: *account_state.repr_hash(),
        })?;
        let transaction = self
            .build_transaction(end_status, state_update)
            .and_then(|tx| Lazy::new(&tx))?;

        // Collect brief transaction info.
        let transaction_meta = TransactionMeta {
            total_fees: self.exec.total_fees,
            next_lt: self.exec.end_lt,
            out_msgs: self.exec.out_msgs,
        };

        // New shard account state.
        let new_state = ShardAccount {
            // SAFETY: `account_state` is an ordinary cell.
            account: unsafe { Lazy::from_raw_unchecked(account_state) },
            last_trans_hash: *transaction.repr_hash(),
            last_trans_lt: self.exec.start_lt,
        };

        // Done
        Ok(ExecutorOutput {
            new_state,
            new_state_meta,
            transaction,
            transaction_meta,
            burned: self.exec.burned,
        })
    }

    fn build_account_state(&self) -> Result<Option<AccountState>> {
        Ok(match self.exec.end_status {
            // Account was deleted.
            AccountStatus::NotExists => None,
            // Uninit account with zero balance is also treated as deleted.
            AccountStatus::Uninit if self.exec.balance.is_zero() => None,
            // Uninit account stays the same.
            AccountStatus::Uninit => Some(AccountState::Uninit),
            // Active account must have a known active state.
            AccountStatus::Active => {
                debug_assert!(matches!(self.exec.state, AccountState::Active(_)));
                Some(self.exec.state.clone())
            }
            // Normalize frozen state.
            AccountStatus::Frozen => {
                let cell;
                let frozen_hash = match &self.exec.state {
                    // Uninit accounts can't be frozen, but if they accidentialy can
                    // just use the account address as frozen state hash to produce the
                    // same uninit state.
                    AccountState::Uninit => &self.exec.address.address,
                    // To freeze an active account we must compute a hash of its state.
                    AccountState::Active(state_init) => {
                        cell = CellBuilder::build_from(state_init)?;
                        cell.repr_hash()
                    }
                    // Account is already frozen.
                    AccountState::Frozen(hash_bytes) => hash_bytes,
                };

                // Normalize account state.
                Some(if frozen_hash == &self.exec.address.address {
                    AccountState::Uninit
                } else {
                    AccountState::Frozen(*frozen_hash)
                })
            }
        })
    }

    fn build_transaction(
        &self,
        end_status: AccountStatus,
        state_update: Lazy<HashUpdate>,
    ) -> Result<Transaction, Error> {
        Ok(Transaction {
            account: self.exec.address.address,
            lt: self.exec.start_lt,
            prev_trans_hash: self.original.last_trans_hash,
            prev_trans_lt: self.original.last_trans_lt,
            now: self.exec.params.block_unixtime,
            out_msg_count: Uint15::new(self.exec.out_msgs.len() as _),
            orig_status: self.exec.orig_status,
            end_status,
            in_msg: self.in_msg.clone(),
            out_msgs: build_out_msgs(&self.exec.out_msgs)?,
            total_fees: self.exec.total_fees.into(),
            state_update,
            info: self.info.clone(),
        })
    }
}

fn compute_storage_used(
    mut prev: Option<(StorageUsed, CellSlice<'_>)>,
    mut new_storage: CellSlice<'_>,
    cache: &mut Option<OwnedExtStorageStat>,
    without_extra_currencies: bool,
) -> Result<StorageUsed> {
    fn skip_extra(slice: &mut CellSlice<'_>) -> Result<bool, Error> {
        let mut cs = *slice;
        cs.skip_first(64, 0)?; // skip lt
        let balance = CurrencyCollection::load_from(&mut cs)?;
        Ok(if balance.other.is_empty() {
            false
        } else {
            slice.skip_first(0, 1)?;
            true
        })
    }

    if without_extra_currencies {
        if let Some((_, prev)) = &mut prev {
            skip_extra(prev)?;
        }
        skip_extra(&mut new_storage)?;
    }

    // Try to reuse previous storage stats if no cells were changed.
    if let Some((prev_used, prev_storage)) = prev {
        'reuse: {
            // Always recompute when previous stats are invalid.
            if prev_used.cells.is_zero()
                || prev_used.bits.into_inner() < prev_storage.size_bits() as u64
            {
                break 'reuse;
            }

            // Simple check that some cells were changed.
            if prev_storage.size_refs() != new_storage.size_refs() {
                break 'reuse;
            }

            // Compare each cell.
            for (prev, new) in prev_storage.references().zip(new_storage.references()) {
                if prev != new {
                    break 'reuse;
                }
            }

            // Adjust bit size of the root slice.
            return Ok(StorageUsed {
                // Compute the truncated difference of the previous storage root and a new one.
                bits: new_varuint56_truncate(
                    (prev_used.bits.into_inner() - prev_storage.size_bits() as u64)
                        .saturating_add(new_storage.size_bits() as u64),
                ),
                // All other stats are unchanged.
                cells: prev_used.cells,
            });
        }
    }

    // Init cache.
    let cache = cache.get_or_insert_with(OwnedExtStorageStat::unlimited);
    cache.set_unlimited();

    // Compute stats for childern.
    for cell in new_storage.references().cloned() {
        cache.add_cell(cell);
    }
    let stats = cache.stats();

    // Done.
    Ok(StorageUsed {
        cells: new_varuint56_truncate(stats.cell_count.saturating_add(1)),
        bits: new_varuint56_truncate(stats.bit_count.saturating_add(new_storage.size_bits() as _)),
    })
}

/// Committed transaction output.
#[derive(Clone, Debug)]
pub struct ExecutorOutput {
    pub new_state: ShardAccount,
    pub new_state_meta: AccountMeta,
    pub transaction: Lazy<Transaction>,
    pub transaction_meta: TransactionMeta,
    pub burned: Tokens,
}

/// Short account description.
#[derive(Clone, Debug)]
pub struct AccountMeta {
    pub balance: CurrencyCollection,
    pub libraries: Dict<HashBytes, SimpleLib>,
    pub exists: bool,
}

/// Short transaction description.
#[derive(Clone, Debug)]
pub struct TransactionMeta {
    /// A sum of all collected fees from all phases.
    pub total_fees: Tokens,
    /// List of outbound messages.
    pub out_msgs: Vec<Lazy<OwnedMessage>>,
    /// Minimal logical time for the next transaction on this account.
    pub next_lt: u64,
}

/// Message cell source.
pub trait LoadMessage {
    fn load_message_root(self) -> Result<Cell>;
}

impl<T: LoadMessage + Clone> LoadMessage for &T {
    #[inline]
    fn load_message_root(self) -> Result<Cell> {
        T::load_message_root(T::clone(self))
    }
}

impl LoadMessage for Cell {
    #[inline]
    fn load_message_root(self) -> Result<Cell> {
        Ok(self)
    }
}

impl<T: EquivalentRepr<OwnedMessage>> LoadMessage for Lazy<T> {
    #[inline]
    fn load_message_root(self) -> Result<Cell> {
        Ok(self.into_inner())
    }
}

impl LoadMessage for OwnedMessage {
    #[inline]
    fn load_message_root(self) -> Result<Cell> {
        CellBuilder::build_from(self).context("failed to serialize inbound message")
    }
}

impl LoadMessage for Message<'_> {
    #[inline]
    fn load_message_root(self) -> Result<Cell> {
        CellBuilder::build_from(self).context("failed to serialize inbound message")
    }
}

fn build_out_msgs(out_msgs: &[Lazy<OwnedMessage>]) -> Result<Dict<Uint15, Cell>, Error> {
    dict::build_dict_from_sorted_iter(
        out_msgs
            .iter()
            .enumerate()
            .map(|(i, msg)| (Uint15::new(i as _), msg.inner().clone())),
        Cell::empty_context(),
    )
    .map(Dict::from_raw)
}

#[cfg(test)]
mod tests {
    use std::rc::Rc;

    use tycho_types::boc::BocRepr;
    use tycho_types::models::{BlockchainConfig, MsgInfo, StateInit};

    use super::*;

    pub fn make_default_config() -> Rc<ParsedConfig> {
        thread_local! {
            pub static PARSED_CONFIG: Rc<ParsedConfig> = make_custom_config(|_| Ok(()));
        }

        PARSED_CONFIG.with(Clone::clone)
    }

    pub fn make_custom_config<F>(f: F) -> Rc<ParsedConfig>
    where
        F: FnOnce(&mut BlockchainConfig) -> anyhow::Result<()>,
    {
        let mut config: BlockchainConfig =
            BocRepr::decode(include_bytes!("../res/config.boc")).unwrap();

        config.params.set_global_id(100).unwrap();

        // TODO: Update config BOC
        config
            .params
            .set_size_limits(&ParsedConfig::DEFAULT_SIZE_LIMITS_CONFIG)
            .unwrap();

        f(&mut config).unwrap();

        Rc::new(ParsedConfig::parse(config, u32::MAX).unwrap())
    }

    pub fn make_default_params() -> ExecutorParams {
        ExecutorParams {
            block_unixtime: 1738799198,
            full_body_in_bounced: false,
            strict_extra_currency: true,
            vm_modifiers: tycho_vm::BehaviourModifiers {
                chksig_always_succeed: true,
                ..Default::default()
            },
            ..Default::default()
        }
    }

    pub fn make_message(
        info: impl Into<MsgInfo>,
        init: Option<StateInit>,
        body: Option<CellBuilder>,
    ) -> Cell {
        let body = match &body {
            None => Cell::empty_cell_ref().as_slice_allow_exotic(),
            Some(cell) => cell.as_full_slice(),
        };
        CellBuilder::build_from(Message {
            info: info.into(),
            init,
            body,
            layout: None,
        })
        .unwrap()
    }

    pub fn make_big_tree(depth: u8, count: &mut u16, target: u16) -> Cell {
        *count += 1;

        if depth == 0 {
            CellBuilder::build_from(*count).unwrap()
        } else {
            let mut b = CellBuilder::new();
            for _ in 0..4 {
                if *count < target {
                    b.store_reference(make_big_tree(depth - 1, count, target))
                        .unwrap();
                }
            }
            b.build().unwrap()
        }
    }

    /// Can be used to replay TON transactions on the Tycho executor.
    #[allow(unused)]
    fn replay_tx(config: &str, state: &str, msg: &str, lt: u64, now: u32) -> Result<()> {
        use tycho_types::models::*;

        let mut config: BlockchainConfig = BocRepr::decode_base64(config)?;
        let state: ShardAccount = BocRepr::decode_base64(state)?;
        let msg = Boc::decode_base64(msg)?;

        let mut workchains = Dict::<i32, _>::new();
        workchains.set(0, WorkchainDescription {
            enabled_since: 0,
            actual_min_split: 0,
            min_split: 0,
            max_split: 0,
            active: true,
            accept_msgs: true,
            zerostate_root_hash: HashBytes::ZERO,
            zerostate_file_hash: HashBytes::ZERO,
            version: 0,
            format: WorkchainFormat::Basic(WorkchainFormatBasic {
                vm_mode: 0,
                vm_version: 0,
            }),
        })?;
        config.params.set_workchains(&workchains)?;
        config.params.remove(43)?;

        let (is_external, address) = match msg.parse::<MsgInfo>()? {
            MsgInfo::Int(info) => (false, info.dst),
            MsgInfo::ExtIn(info) => (true, info.dst),
            MsgInfo::ExtOut(_) => anyhow::bail!("unexpected message"),
        };
        let IntAddr::Std(address) = address else {
            anyhow::bail!("unexpected address");
        };

        let params = ExecutorParams {
            block_unixtime: now,
            ..Default::default()
        };

        let parsed_config = ParsedConfig::parse(config, now).context("failed to parse config")?;
        let output = Executor::new(&params, &parsed_config)
            .with_min_lt(lt)
            .begin_ordinary(&address, is_external, msg, &state)?
            .commit()?;

        let tx = output.transaction.load()?;
        let tx_info = tx.load_info()?;
        println!("{tx_info:#?}");
        Ok(())
    }
}
