use std::time::Duration;

use num_bigint::BigUint;
use num_integer::Integer;
use num_rational::BigRational;
use num_traits::ToPrimitive;
use tycho_types::models::{
    ShardIdent, WorkUnitsParams, WorkUnitsParamsExecute, WorkUnitsParamsFinalize,
    WorkUnitsParamsPrepare,
};

use crate::collator::messages_reader::MessagesReaderMetrics;
use crate::collator::types::{ExecuteMetrics, FinalizeMetrics};
use crate::tracing_targets;
use crate::types::McData;
use crate::types::processed_upto::BlockSeqno;

pub const EXECUTE_OVER_THREADS_SCALE: u128 = 10_000;

#[derive(Default)]
pub struct PrepareMsgGroupsWu {
    pub total_elapsed: Duration,

    pub fixed_part: u64,

    pub read_ext_msgs_count: u64,
    pub read_ext_msgs_wu: u64,
    pub read_ext_msgs_elapsed: Duration,
    /// Count-based base used to calculate target `read_ext_msgs` param
    pub read_ext_msgs_base: u128,

    pub read_existing_int_msgs_count: u64,
    pub read_existing_int_msgs_wu: u64,
    pub read_existing_int_msgs_elapsed: Duration,
    /// Count-based base used to calculate target `read_int_msgs` param
    pub read_existing_int_msgs_base: u128,

    pub read_new_int_msgs_count: u64,
    pub read_new_int_msgs_wu: u64,
    pub read_new_int_msgs_elapsed: Duration,
    /// Count-based base used to calculate `read_new_msgs` param
    pub read_new_int_msgs_base: u128,

    pub add_to_msgs_groups_ops_count: u64,
    pub add_msgs_to_groups_wu: u64,
    pub add_msgs_to_groups_elapsed: Duration,
    /// Count-based base used to calculate `add_to_msg_groups` param
    pub add_msgs_to_groups_base: u128,
}

impl PrepareMsgGroupsWu {
    pub(super) fn calculate(
        wu_params_prepare: &WorkUnitsParamsPrepare,
        msgs_reader_metrics: &MessagesReaderMetrics,
        prepare_msg_groups_total_elapsed: Duration,
    ) -> Self {
        let &WorkUnitsParamsPrepare {
            fixed_part,
            read_ext_msgs,
            read_int_msgs,
            read_new_msgs,
            add_to_msg_groups,
            ..
        } = wu_params_prepare;

        let &MessagesReaderMetrics {
            read_ext_msgs_count,
            read_existing_msgs_count: read_existing_int_msgs_count,
            read_new_msgs_count: read_new_int_msgs_count,
            add_to_msgs_groups_ops_count,
            ..
        } = msgs_reader_metrics;

        let read_ext_msgs_wu = read_ext_msgs_count.saturating_mul(read_ext_msgs as u64);
        let read_existing_int_msgs_wu =
            read_existing_int_msgs_count.saturating_mul(read_int_msgs as u64);
        let read_new_int_msgs_wu = read_new_int_msgs_count.saturating_mul(read_new_msgs as u64);
        let add_msgs_to_groups_wu =
            add_to_msgs_groups_ops_count.saturating_mul(add_to_msg_groups as u64);

        Self {
            total_elapsed: prepare_msg_groups_total_elapsed,

            fixed_part: fixed_part as u64,

            read_ext_msgs_count,
            read_ext_msgs_wu,
            read_ext_msgs_elapsed: msgs_reader_metrics.read_ext_messages_timer.total_elapsed,
            read_ext_msgs_base: read_ext_msgs_count as u128,

            read_existing_int_msgs_count,
            read_existing_int_msgs_wu,
            read_existing_int_msgs_elapsed: msgs_reader_metrics
                .read_existing_messages_timer
                .total_elapsed,
            read_existing_int_msgs_base: read_existing_int_msgs_count as u128,

            read_new_int_msgs_count,
            read_new_int_msgs_wu,
            read_new_int_msgs_elapsed: msgs_reader_metrics.read_new_messages_timer.total_elapsed,
            read_new_int_msgs_base: read_new_int_msgs_count as u128,

            add_to_msgs_groups_ops_count,
            add_msgs_to_groups_wu,
            add_msgs_to_groups_elapsed: msgs_reader_metrics
                .add_to_message_groups_timer
                .total_elapsed,
            add_msgs_to_groups_base: add_to_msgs_groups_ops_count as u128,
        }
    }

    pub fn log_wu_metrics(&self, wu_params_prepare: &WorkUnitsParamsPrepare) {
        let &WorkUnitsParamsPrepare {
            read_ext_msgs,
            read_int_msgs,
            read_new_msgs,
            add_to_msg_groups,
            ..
        } = wu_params_prepare;

        tracing::debug!(target: tracing_targets::COLLATOR,
            "read_msg_groups_wu: total: (wu={}, elapsed={}, price={}), \
            read_ext_msgs: (count={}, param={}, wu={}, elapsed={}, price={}), \
            read_existing_int_msgs: (count={}, param={}, wu={}, elapsed={}, price={}), \
            read_new_int_msgs: (count={}, param={}, wu={}, elapsed={}, price={}), \
            add_to_msgs_groups_ops: (count={}, param={}, wu={}, elapsed={}, price={})",
            self.total_wu(), self.total_elapsed.as_nanos(), self.total_wu_price(),
            self.read_ext_msgs_count, read_ext_msgs, self.read_ext_msgs_wu,
            self.read_ext_msgs_elapsed.as_nanos(), self.read_ext_msgs_wu_price(),
            self.read_existing_int_msgs_count, read_int_msgs, self.read_existing_int_msgs_wu,
            self.read_existing_int_msgs_elapsed.as_nanos(), self.read_existing_int_msgs_wu_price(),
            self.read_new_int_msgs_count, read_new_msgs, self.read_new_int_msgs_wu,
            self.read_new_int_msgs_elapsed.as_nanos(), self.read_new_int_msgs_wu_price(),
            self.add_to_msgs_groups_ops_count, add_to_msg_groups, self.add_msgs_to_groups_wu,
            self.add_msgs_to_groups_elapsed.as_nanos(), self.add_msgs_to_groups_wu_price(),
        );
    }

    pub fn total_wu(&self) -> u64 {
        self.fixed_part
            .saturating_add(self.read_ext_msgs_wu)
            .saturating_add(self.read_existing_int_msgs_wu)
            .saturating_add(self.read_new_int_msgs_wu)
            .saturating_add(self.add_msgs_to_groups_wu)
    }

    pub fn total_wu_price(&self) -> f64 {
        let total_wu = self.total_wu();
        if total_wu == 0 {
            return 0.0;
        }
        self.total_elapsed.as_nanos() as f64 / total_wu as f64
    }

    pub fn read_ext_msgs_wu_price(&self) -> f64 {
        if self.read_ext_msgs_wu == 0 {
            return 0.0;
        }
        self.read_ext_msgs_elapsed.as_nanos() as f64 / self.read_ext_msgs_wu as f64
    }

    pub fn read_existing_int_msgs_wu_price(&self) -> f64 {
        if self.read_existing_int_msgs_wu == 0 {
            return 0.0;
        }
        self.read_existing_int_msgs_elapsed.as_nanos() as f64
            / self.read_existing_int_msgs_wu as f64
    }

    pub fn read_new_int_msgs_wu_price(&self) -> f64 {
        if self.read_new_int_msgs_wu == 0 {
            return 0.0;
        }
        self.read_new_int_msgs_elapsed.as_nanos() as f64 / self.read_new_int_msgs_wu as f64
    }

    pub fn add_msgs_to_groups_wu_price(&self) -> f64 {
        if self.add_msgs_to_groups_wu == 0 {
            return 0.0;
        }
        self.add_msgs_to_groups_elapsed.as_nanos() as f64 / self.add_msgs_to_groups_wu as f64
    }
}

#[derive(Default)]
pub struct ExecuteWu {
    pub in_msgs_len: u64,
    pub out_msgs_len: u64,
    pub inserted_new_msgs_count: u64,

    pub groups_count: u64,

    /// Stores the accounts term normalized by the actual threads count for the target `execute` param calculation
    pub execute_groups_vm_sum_accounts_over_threads: u128,
    /// Stores the gas term normalized by the actual threads count for the target `execute` param calculation
    pub execute_groups_vm_sum_gas_over_threads: u128,

    pub execute_groups_vm_only_wu: u64,
    pub execute_groups_vm_only_elapsed: Duration,

    /// Shared `n * log2(n)` base used to calculate target params
    pub process_txs_base: u128,
    pub process_txs_wu: u64,
    pub process_txs_elapsed: Duration,
}
impl ExecuteWu {
    pub fn append_group_exec_wu(
        &mut self,
        wu_params_execute: &WorkUnitsParamsExecute,
        group_accounts_count: u64,
        group_gas: u128,
    ) -> u128 {
        let &WorkUnitsParamsExecute {
            prepare,
            execute,
            execute_delimiter,
            subgroup_size: max_threads_count,
            ..
        } = wu_params_execute;

        self.groups_count += 1;
        // we cannot use more thread then groups count
        let threads_count = calc_threads_count(max_threads_count as u64, group_accounts_count);

        // keep normalized accounts and gas sums for the target param calculation
        self.execute_groups_vm_sum_accounts_over_threads = self
            .execute_groups_vm_sum_accounts_over_threads
            .saturating_add(
                (group_accounts_count as u128)
                    .saturating_mul(EXECUTE_OVER_THREADS_SCALE)
                    .saturating_div(threads_count as u128),
            );
        self.execute_groups_vm_sum_gas_over_threads =
            self.execute_groups_vm_sum_gas_over_threads.saturating_add(
                group_gas
                    .saturating_mul(EXECUTE_OVER_THREADS_SCALE)
                    .saturating_div(threads_count as u128),
            );

        // calculate total group execute wu by sum gas and accounts count
        let group_exec_wu = (group_accounts_count as u128)
            .saturating_mul(prepare as u128)
            .saturating_add(
                group_gas
                    .saturating_mul(execute as u128)
                    .saturating_div(execute_delimiter as u128),
            )
            .saturating_div(threads_count as u128);

        self.execute_groups_vm_only_wu = self
            .execute_groups_vm_only_wu
            .saturating_add(group_exec_wu as u64);

        group_exec_wu
    }

    pub fn calculate_process_txs_wu(
        &mut self,
        wu_params_execute: &WorkUnitsParamsExecute,
        in_msgs_len: u64,
        out_msgs_len: u64,
        inserted_new_msgs_count: u64,
    ) {
        let &WorkUnitsParamsExecute {
            serialize_enqueue: insert_in_msgs,
            serialize_dequeue: insert_out_msgs,
            insert_new_msgs,
            ..
        } = wu_params_execute;

        self.in_msgs_len = in_msgs_len;
        let in_msgs_len_log = self.in_msgs_len_log();
        self.out_msgs_len = out_msgs_len;
        let out_msgs_len_log = self.out_msgs_len_log();
        self.inserted_new_msgs_count = inserted_new_msgs_count;
        let inserted_new_msgs_count_log = self.inserted_new_msgs_count_log();

        // use `n * log2(n)` complexity to eval because queue serialization cost grows with collection size
        self.process_txs_base = (in_msgs_len as u128)
            .saturating_mul(in_msgs_len_log as u128)
            .saturating_add((out_msgs_len as u128).saturating_mul(out_msgs_len_log as u128))
            .saturating_add(
                (inserted_new_msgs_count as u128)
                    .saturating_mul(inserted_new_msgs_count_log as u128),
            );

        self.process_txs_wu = in_msgs_len
            .saturating_mul(in_msgs_len_log)
            .saturating_mul(insert_in_msgs as u64)
            .saturating_add(
                out_msgs_len
                    .saturating_mul(out_msgs_len_log)
                    .saturating_mul(insert_out_msgs as u64),
            )
            .saturating_add(
                inserted_new_msgs_count
                    .saturating_mul(inserted_new_msgs_count_log)
                    .saturating_mul(insert_new_msgs as u64),
            );
    }

    pub(super) fn append_elapsed_timings(&mut self, execute_metrics: &ExecuteMetrics) {
        self.execute_groups_vm_only_elapsed =
            execute_metrics.execute_groups_vm_only_timer.total_elapsed;
        self.process_txs_elapsed = execute_metrics.process_txs_timer.total_elapsed;
    }

    pub fn in_msgs_len_log(&self) -> u64 {
        self.in_msgs_len.checked_ilog2().unwrap_or_default() as u64
    }

    pub fn out_msgs_len_log(&self) -> u64 {
        self.out_msgs_len.checked_ilog2().unwrap_or_default() as u64
    }

    pub fn inserted_new_msgs_count_log(&self) -> u64 {
        self.inserted_new_msgs_count
            .checked_ilog2()
            .unwrap_or_default() as u64
    }

    pub fn total_wu(&self) -> u64 {
        self.execute_groups_vm_only_wu
            .saturating_add(self.process_txs_wu)
    }
    pub fn total_elapsed(&self) -> Duration {
        self.execute_groups_vm_only_elapsed
            .saturating_add(self.process_txs_elapsed)
    }
    pub fn total_wu_price(&self) -> f64 {
        let total_wu = self.total_wu();
        if total_wu == 0 {
            return 0.0;
        }
        self.total_elapsed().as_nanos() as f64 / total_wu as f64
    }

    /// Calcs target `execute.execute` wu param.
    pub fn calc_target_execute_wu_param(
        target_wu_price: (BigUint, BigUint),
        prepare_wu_param: u64,
        execute_delimiter: u64,
        sums_accum: &WuAccumSumsExecute,
    ) -> Option<u64> {
        if sums_accum.is_sum_gas_over_threads_zero() {
            return None;
        }
        let (price_num, price_den) = target_wu_price;
        let gas = sums_accum.sum_gas_over_threads();
        let prepare = sums_accum.sum_accounts_over_threads() * prepare_wu_param;

        // elapsed = price * (prepare_p * accounts + execute * gas / execute_delimiter) / EXECUTE_OVER_THREADS_SCALE
        // -> elapsed = price * (prepare + execute * gas / execute_delimiter) / EXECUTE_OVER_THREADS_SCALE
        // -> elapsed = price_num / price_den * (prepare + execute * gas / execute_delimiter) / EXECUTE_OVER_THREADS_SCALE
        // -> elapsed * EXECUTE_OVER_THREADS_SCALE * price_den / price_num = prepare + execute * gas / execute_delimiter
        // -> elapsed_scaled = elapsed * EXECUTE_OVER_THREADS_SCALE * price_den
        // -> elapsed_scaled / price_num = prepare + execute * gas / execute_delimiter
        // -> elapsed_scaled = elapsed_scaled_q * price_num + elapsed_scaled_r
        // -> (elapsed_scaled_q * price_num + elapsed_scaled_r) / price_num = prepare + execute * gas / execute_delimiter
        // -> elapsed_scaled_q + elapsed_scaled_r / price_num = prepare + execute * gas / execute_delimiter
        // -> execute = ((elapsed_scaled_q - prepare) + elapsed_scaled_r / price_num) * execute_delimiter / gas
        // -> whole = (elapsed_scaled_q - prepare) * execute_delimiter
        // -> execute = whole / gas + elapsed_scaled_r * execute_delimiter / (gas * price_num)
        // -> execute = (whole_q * gas + whole_r) / gas + elapsed_scaled_r * execute_delimiter / (gas * price_num)
        // -> execute = whole_q + whole_r / gas + elapsed_scaled_r * execute_delimiter / (gas * price_num)
        // -> execute = whole_q + whole_r * price_num / (gas * price_num) + elapsed_scaled_r * execute_delimiter / (gas * price_num)
        // -> execute = whole_q + (whole_r * price_num + elapsed_scaled_r * execute_delimiter) / (gas * price_num)

        let elapsed_scaled =
            sums_accum.sum_elapsed_clip_ns() * EXECUTE_OVER_THREADS_SCALE * &price_den;
        let (elapsed_scaled_q, elapsed_scaled_r) = elapsed_scaled.div_rem(&price_num);

        // if the prepare term already exceeds the available whole part then the execute-only part would be negative
        if elapsed_scaled_q < prepare {
            return None;
        }

        let whole = (elapsed_scaled_q - prepare) * execute_delimiter;
        let (whole_q, whole_r) = whole.div_rem(gas);

        // calculate target param with truncation of the fractional part
        let target_param = whole_q
            + (whole_r * &price_num + elapsed_scaled_r * execute_delimiter) / (gas * price_num);

        Some(target_param.to_u64().unwrap_or(u64::MAX))
    }

    pub fn execute_groups_vm_only_wu_price(&self) -> f64 {
        if self.execute_groups_vm_only_wu == 0 {
            return 0.0;
        }
        self.execute_groups_vm_only_elapsed.as_nanos() as f64
            / self.execute_groups_vm_only_wu as f64
    }

    pub fn process_txs_wu_price(&self) -> f64 {
        if self.process_txs_wu == 0 {
            return 0.0;
        }
        self.process_txs_elapsed.as_nanos() as f64 / self.process_txs_wu as f64
    }
}

#[derive(Default, Clone)]
pub struct FinalizeWu {
    pub diff_msgs_count: u64,
    /// Actullay, `diff_msgs_count` is used as the base
    pub create_diff_base: u128,
    /// Actullay, `diff_msgs_count` is used as the base
    pub apply_diff_base: u128,

    pub create_queue_diff_wu: u64,
    pub create_queue_diff_elapsed: Duration,

    pub apply_queue_diff_wu: u64,
    pub apply_queue_diff_elapsed: Duration,

    pub shard_accounts_count: u64,
    pub updated_accounts_count: u64,
    pub in_msgs_len: u64,
    pub out_msgs_len: u64,

    pub update_shard_accounts_wu: u64,
    pub update_shard_accounts_elapsed: Duration,
    /// Proportional base used to calculate target `build_accounts` param
    pub update_shard_accounts_base: u128,

    pub build_accounts_blocks_wu: u64,
    pub build_accounts_blocks_elapsed: Duration,
    /// Proportional base used to calculate target `build_transactions` param
    pub build_accounts_blocks_base: u128,

    pub build_accounts_elapsed: Duration,

    pub build_in_msgs_wu: u64,
    pub build_in_msgs_elapsed: Duration,
    /// Proportional base used to calculate target `build_in_msg` param
    pub build_in_msg_base: u128,

    pub build_out_msgs_wu: u64,
    pub build_out_msgs_elapsed: Duration,
    /// Proportional base used to calculate target `build_out_msg` param
    pub build_out_msg_base: u128,

    pub build_accounts_and_messages_in_parallel_elased: Duration,

    pub build_state_update_wu: u64,
    pub build_state_update_elapsed: Duration,
    /// Proportional base used to calculate target `state_update_accounts` param
    pub build_state_update_base: u128,

    pub build_block_wu: u64,
    pub build_block_elapsed: Duration,
    /// Proportional base used to calculate target serialize params
    pub build_block_base: u128,

    pub finalize_block_elapsed: Duration,

    pub total_elapsed: Duration,
}
impl FinalizeWu {
    pub fn calculate_queue_diff_wu(
        &mut self,
        wu_params_finalize: &WorkUnitsParamsFinalize,
        diff_msgs_count: u64,
    ) {
        let &WorkUnitsParamsFinalize {
            create_diff,
            apply_diff,
            ..
        } = wu_params_finalize;

        self.diff_msgs_count = diff_msgs_count;
        self.create_diff_base = diff_msgs_count as u128;
        self.apply_diff_base = diff_msgs_count as u128;

        self.create_queue_diff_wu = diff_msgs_count.saturating_mul(create_diff as u64);
        self.apply_queue_diff_wu = diff_msgs_count.saturating_mul(apply_diff as u64);
    }

    pub fn calculate_finalize_block_wu(
        &mut self,
        wu_params_finalize: &WorkUnitsParamsFinalize,
        wu_params_execute: &WorkUnitsParamsExecute,
        shard_accounts_count: u64,
        updated_accounts_count: u64,
        in_msgs_len: u64,
        out_msgs_len: u64,
    ) {
        let &WorkUnitsParamsFinalize {
            build_accounts,
            build_transactions,
            build_in_msg,
            build_out_msg,
            serialize_min,
            serialize_accounts,
            serialize_msg,
            state_update_min,
            state_update_accounts,
            state_update_msg: state_pow_coeff,
            ..
        } = wu_params_finalize;

        let &WorkUnitsParamsExecute {
            subgroup_size: max_threads_count,
            ..
        } = wu_params_execute;

        let threads_count = calc_threads_count(max_threads_count as u64, updated_accounts_count);

        self.in_msgs_len = in_msgs_len;
        self.out_msgs_len = out_msgs_len;
        self.updated_accounts_count = updated_accounts_count;
        self.shard_accounts_count = shard_accounts_count;

        let shard_accounts_count_log = self.shard_accounts_count_log();

        let scale = 10;
        let pow_shard_accounts_count = self.pow_shard_accounts_count(state_pow_coeff, scale);

        // calc update shard accounts
        self.calc_update_shard_accounts_wu(
            threads_count,
            shard_accounts_count_log,
            pow_shard_accounts_count,
            scale,
            build_accounts as u64,
        );

        // calc build account blocks
        self.calc_build_accounts_blocks_wu(threads_count, build_transactions as u64);

        // calc build in msgs
        self.calc_build_in_msgs_wu(build_in_msg as u64);

        // calc build out msgs
        self.calc_build_out_msgs_wu(build_out_msg as u64);

        // calc build state update
        self.calc_build_state_update_wu(
            threads_count,
            shard_accounts_count_log,
            pow_shard_accounts_count,
            scale,
            state_update_min as u64,
            state_update_accounts as u64,
        );

        // calc build block
        self.calc_build_block_wu(
            serialize_min as u64,
            serialize_accounts as u64,
            serialize_msg as u64,
        );
    }

    pub(super) fn append_elapsed_timings(&mut self, finalize_metrics: &FinalizeMetrics) {
        self.create_queue_diff_elapsed = finalize_metrics.create_queue_diff_timer.total_elapsed;
        self.apply_queue_diff_elapsed = finalize_metrics.apply_queue_diff_elapsed;

        self.update_shard_accounts_elapsed = finalize_metrics.update_shard_accounts_elapsed;
        self.build_accounts_blocks_elapsed = finalize_metrics.build_accounts_blocks_elapsed;

        self.build_accounts_elapsed = finalize_metrics.build_accounts_elapsed;

        self.build_in_msgs_elapsed = finalize_metrics.build_in_msgs_elapsed;
        self.build_out_msgs_elapsed = finalize_metrics.build_out_msgs_elapsed;

        self.build_accounts_and_messages_in_parallel_elased =
            finalize_metrics.build_accounts_and_messages_in_parallel_elased;

        self.build_state_update_elapsed = finalize_metrics.build_state_update_elapsed;
        self.build_block_elapsed = finalize_metrics.build_block_elapsed;

        self.finalize_block_elapsed = finalize_metrics.finalize_block_elapsed;

        self.total_elapsed = finalize_metrics.total_timer.total_elapsed;
    }

    pub fn shard_accounts_count_log(&self) -> u64 {
        self.shard_accounts_count
            .checked_ilog2()
            .unwrap_or_default() as u64
    }

    pub fn pow_shard_accounts_count(&self, state_pow_coeff: u16, scale: u128) -> u128 {
        let (numerator, denominator) = unpack_from_u16(state_pow_coeff);
        compute_scaled_pow(
            self.shard_accounts_count as u128,
            numerator as u32,
            denominator as u32,
            scale,
        )
    }

    pub fn updated_accounts_count_log(&self) -> u64 {
        self.updated_accounts_count
            .checked_ilog2()
            .unwrap_or_default() as u64
    }

    fn calc_update_shard_accounts_wu(
        &mut self,
        threads_count: u64,
        shard_accounts_count_log: u64,
        pow_shard_accounts_count: u128,
        scale: u128,
        update_shard_accounts_wu_param: u64,
    ) {
        self.update_shard_accounts_base = self.calc_update_shard_accounts_wu_base(
            threads_count,
            shard_accounts_count_log,
            pow_shard_accounts_count,
            scale,
        );
        self.update_shard_accounts_wu = self
            .update_shard_accounts_base
            .saturating_mul(update_shard_accounts_wu_param as u128)
            .saturating_div(scale) as u64;
    }

    fn calc_update_shard_accounts_wu_base(
        &self,
        threads_count: u64,
        shard_accounts_count_log: u64,
        pow_shard_accounts_count: u128,
        scale: u128,
    ) -> u128 {
        //  * prepare account modifications in (updated_accounts_count)
        //  * then update map of shard accounts in parallel in (updated_accounts_count)*log(shard_accounts_count)/(threads_count)
        //  * additionally multiply on (shard_accounts_count^a) for better precision on a large state
        (self.updated_accounts_count as u128)
            .saturating_mul(scale)
            .saturating_add(
                (self.updated_accounts_count as u128)
                    .saturating_mul(shard_accounts_count_log as u128)
                    .saturating_div(threads_count as u128)
                    .saturating_mul(pow_shard_accounts_count),
            )
    }

    pub fn update_shard_accounts_wu_price(&self) -> f64 {
        if self.update_shard_accounts_wu == 0 {
            return 0.0;
        }
        self.update_shard_accounts_elapsed.as_nanos() as f64 / self.update_shard_accounts_wu as f64
    }

    fn calc_build_accounts_blocks_wu(
        &mut self,
        threads_count: u64,
        build_accounts_blocks_wu_param: u64,
    ) {
        let updated_accounts_count_log = self.updated_accounts_count_log();
        self.build_accounts_blocks_base = self
            .calc_build_accounts_blocks_wu_base(threads_count, updated_accounts_count_log)
            as u128;
        self.build_accounts_blocks_wu = self
            .build_accounts_blocks_base
            .saturating_mul(build_accounts_blocks_wu_param as u128)
            .min(u64::MAX as u128) as u64;
    }

    fn calc_build_accounts_blocks_wu_base(
        &self,
        threads_count: u64,
        updated_accounts_count_log: u64,
    ) -> u64 {
        //  * build maps of transactions by accounts in parallel in (txs_count)*log(txs_count/threads_count)/(threads_count)
        //  * then build AugDict of accounts blocks in (updated_accounts_count)*log(updated_accounts_count)

        // the transaction-map term benefits from threads while the final accounts-block dict build does not
        let in_msgs_len_div_threads_count_log =
            self.in_msgs_len_div_threads_count_log(threads_count);
        self.in_msgs_len
            .saturating_mul(in_msgs_len_div_threads_count_log)
            .saturating_div(threads_count)
            .saturating_add(
                self.updated_accounts_count
                    .saturating_mul(updated_accounts_count_log),
            )
    }

    pub fn build_accounts_blocks_wu_price(&self) -> f64 {
        if self.build_accounts_blocks_wu == 0 {
            return 0.0;
        }
        self.build_accounts_blocks_elapsed.as_nanos() as f64 / self.build_accounts_blocks_wu as f64
    }

    pub fn build_accounts_wu(&self) -> u64 {
        self.update_shard_accounts_wu + self.build_accounts_blocks_wu
    }

    pub fn build_accounts_wu_price(&self) -> f64 {
        let build_accounts_wu = self.build_accounts_wu();
        if build_accounts_wu == 0 {
            return 0.0;
        }
        self.build_accounts_elapsed.as_nanos() as f64 / build_accounts_wu as f64
    }

    pub fn in_msgs_len_log(&self) -> u64 {
        self.in_msgs_len.checked_ilog2().unwrap_or_default() as u64
    }

    pub fn in_msgs_len_div_threads_count_log(&self, threads_count: u64) -> u64 {
        self.in_msgs_len
            .saturating_div(threads_count)
            .checked_ilog2()
            .unwrap_or_default() as u64
    }

    fn calc_build_in_msgs_wu(&mut self, build_in_msg_wu_param: u64) {
        let in_msgs_len_log = self.in_msgs_len_log();
        self.build_in_msg_base = self.calc_build_in_msgs_wu_base(in_msgs_len_log) as u128;
        self.build_in_msgs_wu = self
            .build_in_msg_base
            .saturating_mul(build_in_msg_wu_param as u128)
            .min(u64::MAX as u128) as u64;
    }

    fn calc_build_in_msgs_wu_base(&self, in_msgs_len_log: u64) -> u64 {
        //  * build AugDict of in msgs in (in_msgs_len)*log(in_msgs_len)
        self.in_msgs_len.saturating_mul(in_msgs_len_log)
    }

    pub fn build_in_msgs_wu_price(&self) -> f64 {
        if self.build_in_msgs_wu == 0 {
            return 0.0;
        }
        self.build_in_msgs_elapsed.as_nanos() as f64 / self.build_in_msgs_wu as f64
    }

    pub fn out_msgs_len_log(&self) -> u64 {
        self.out_msgs_len.checked_ilog2().unwrap_or_default() as u64
    }

    fn calc_build_out_msgs_wu(&mut self, build_out_msg_wu_param: u64) {
        let out_msgs_len_log = self.out_msgs_len_log();
        self.build_out_msg_base = self.calc_build_out_msgs_wu_base(out_msgs_len_log) as u128;
        self.build_out_msgs_wu = self
            .build_out_msg_base
            .saturating_mul(build_out_msg_wu_param as u128)
            .min(u64::MAX as u128) as u64;
    }

    fn calc_build_out_msgs_wu_base(&self, out_msgs_len_log: u64) -> u64 {
        //  * build AugDict of out msgs in (out_msgs_len)*log(out_msgs_len)
        self.out_msgs_len.saturating_mul(out_msgs_len_log)
    }

    pub fn build_out_msgs_wu_price(&self) -> f64 {
        if self.build_out_msgs_wu == 0 {
            return 0.0;
        }
        self.build_out_msgs_elapsed.as_nanos() as f64 / self.build_out_msgs_wu as f64
    }

    pub fn max_accounts_in_out_msgs_wu(&self) -> u64 {
        self.build_accounts_wu()
            .max(self.build_in_msgs_wu)
            .max(self.build_out_msgs_wu)
    }

    pub fn build_accounts_and_messages_in_parallel_wu_price(&self) -> f64 {
        let max_wu = self.max_accounts_in_out_msgs_wu();
        if max_wu == 0 {
            return 0.0;
        }
        self.build_accounts_and_messages_in_parallel_elased
            .as_nanos() as f64
            / max_wu as f64
    }

    fn calc_build_state_update_wu(
        &mut self,
        threads_count: u64,
        shard_accounts_count_log: u64,
        pow_shard_accounts_count: u128,
        scale: u128,
        build_state_update_wu_min: u64,
        build_state_update_wu_param: u64,
    ) {
        //  * use min value if calculated is less
        self.build_state_update_base = self.calc_build_state_update_wu_base(
            threads_count,
            shard_accounts_count_log,
            pow_shard_accounts_count,
        );
        self.build_state_update_wu = std::cmp::max(
            build_state_update_wu_min,
            self.build_state_update_base
                .saturating_mul(build_state_update_wu_param as u128)
                .saturating_div(scale) as u64,
        );
    }

    fn calc_build_state_update_wu_base(
        &self,
        threads_count: u64,
        shard_accounts_count_log: u64,
        pow_shard_accounts_count: u128,
    ) -> u128 {
        //  * calc update of all updated accounts in parallel in (updated_accounts_count)*log(shard_accounts_count)/(threads_count)
        //  * additionally multiply on (shard_accounts_count^0.1504) for better precision on a large state
        (self.updated_accounts_count as u128)
            .saturating_mul(shard_accounts_count_log as u128)
            .saturating_div(threads_count as u128)
            .saturating_mul(pow_shard_accounts_count)
    }

    pub fn build_state_update_wu_price(&self) -> f64 {
        if self.build_state_update_wu == 0 {
            return 0.0;
        }
        self.build_state_update_elapsed.as_nanos() as f64 / self.build_state_update_wu as f64
    }

    fn calc_build_block_wu(
        &mut self,
        serialize_min_wu: u64,
        serialize_account_wu_param: u64,
        serialize_msg_wu_param: u64,
    ) {
        //  * serialize each account block in (updated_accounts_count)
        //  * serialize in msgs and out msgs in (in_msgs_len + out_msgs_len)
        //  * use min value if calculated is less
        // share one base across account and message serialization while keeping separate params
        self.build_block_base = self
            .updated_accounts_count
            .saturating_add(self.in_msgs_len)
            .saturating_add(self.out_msgs_len) as u128;
        self.build_block_wu = std::cmp::max(
            serialize_min_wu,
            self.updated_accounts_count
                .saturating_mul(serialize_account_wu_param)
                .saturating_add(
                    self.in_msgs_len
                        .saturating_add(self.out_msgs_len)
                        .saturating_mul(serialize_msg_wu_param),
                ),
        );
    }

    pub fn build_block_wu_price(&self) -> f64 {
        if self.build_block_wu == 0 {
            return 0.0;
        }
        self.build_block_elapsed.as_nanos() as f64 / self.build_block_wu as f64
    }

    pub fn finalize_block_wu(&self) -> u64 {
        self.max_accounts_in_out_msgs_wu()
            .saturating_add(self.build_state_update_wu)
            .saturating_add(self.build_block_wu)
    }

    pub fn finalize_block_wu_price(&self) -> f64 {
        let finalize_block_wu = self.finalize_block_wu();
        if finalize_block_wu == 0 {
            return 0.0;
        }
        self.finalize_block_elapsed.as_nanos() as f64 / finalize_block_wu as f64
    }

    pub fn create_queue_diff_wu_price(&self) -> f64 {
        if self.create_queue_diff_wu == 0 {
            return 0.0;
        }
        self.create_queue_diff_elapsed.as_nanos() as f64 / self.create_queue_diff_wu as f64
    }

    pub fn apply_queue_diff_wu_price(&self) -> f64 {
        if self.apply_queue_diff_wu == 0 {
            return 0.0;
        }
        self.apply_queue_diff_elapsed.as_nanos() as f64 / self.apply_queue_diff_wu as f64
    }

    pub fn total_wu(&self) -> u64 {
        // take max from finalize block and apply diff and sum with create diff
        // create diff is sequential while apply diff overlaps with finalize block work, so consider only the slower branch
        self.finalize_block_wu()
            .max(self.apply_queue_diff_wu)
            .saturating_add(self.create_queue_diff_wu)
    }

    pub fn total_wu_price(&self) -> f64 {
        let total_wu = self.total_wu();
        if total_wu == 0 {
            return 0.0;
        }
        self.total_elapsed.as_nanos() as f64 / total_wu as f64
    }
}

#[derive(Default, Clone)]
pub struct DoCollateWu {
    pub shard_accounts_count: u64,
    pub updated_accounts_count: u64,

    pub resume_collation_wu: u64,
    pub resume_collation_elapsed: Duration,
    /// Proportional base used to calculate target `diff_tail_len` param
    pub resume_collation_base: u128,

    /// Resume collation wu spread over shard blocks between two master blocks
    pub resume_collation_wu_per_block: u64,
    /// Resume collation elapsed spread over shard blocks between two master blocks
    pub resume_collation_elapsed_per_block_ns: u128,

    pub collation_total_elapsed: Duration,
}

impl DoCollateWu {
    pub fn calculate_resume_collation_wu(
        &mut self,
        wu_params_finalize: &WorkUnitsParamsFinalize,
        wu_params_execute: &WorkUnitsParamsExecute,
        shard_accounts_count: u64,
        updated_accounts_count: u64,
        mc_data: &McData,
        current_shard: &ShardIdent,
    ) {
        let &WorkUnitsParamsFinalize {
            state_update_msg: state_pow_coeff,
            serialize_diff: updated_accounts_count_pow_coeff,
            diff_tail_len: store_state_wu_param,
            ..
        } = wu_params_finalize;

        let &WorkUnitsParamsExecute {
            subgroup_size: max_threads_count,
            ..
        } = wu_params_execute;

        let threads_count = calc_threads_count(max_threads_count as u64, updated_accounts_count);

        self.shard_accounts_count = shard_accounts_count;
        let shard_accounts_count_log = self.shard_accounts_count_log();
        self.updated_accounts_count = updated_accounts_count;

        let scale = 10;
        let pow_shard_accounts_count = self.pow_shard_accounts_count(state_pow_coeff, scale);
        let pow_updated_accounts_count =
            self.pow_updated_accounts_count(updated_accounts_count_pow_coeff, scale);

        self.calc_resume_collation_wu(
            threads_count,
            pow_updated_accounts_count,
            shard_accounts_count_log,
            pow_shard_accounts_count,
            scale,
            store_state_wu_param as u64,
        );

        // we eval the time spent on saving state updated state to storage,
        // we consider that we actually wait of only one last state store finished,
        // so we spread wu over shard blocks count between masters
        let blocks_count_between_masters = mc_data.get_blocks_count_between_masters(current_shard);
        self.resume_collation_wu_per_block = self
            .resume_collation_wu
            .saturating_div(blocks_count_between_masters.max(1));
        self.resume_collation_elapsed_per_block_ns = self
            .resume_collation_elapsed
            .as_nanos()
            .saturating_div(blocks_count_between_masters.max(1) as u128);
    }

    pub fn shard_accounts_count_log(&self) -> u64 {
        self.shard_accounts_count
            .checked_ilog2()
            .unwrap_or_default() as u64
    }

    pub fn pow_shard_accounts_count(&self, state_pow_coeff: u16, scale: u128) -> u128 {
        let (numerator, denominator) = unpack_from_u16(state_pow_coeff);
        compute_scaled_pow(
            self.shard_accounts_count as u128,
            numerator as u32,
            denominator as u32,
            scale,
        )
    }

    pub fn normalized_updated_accounts_count(&self) -> u64 {
        // floor small diffs so exponent-based resume wu calculation does not collapse on small updates
        if self.updated_accounts_count == 0 {
            0
        } else {
            self.updated_accounts_count.max(4000)
        }
    }

    pub fn pow_updated_accounts_count(&self, state_pow_coeff: u16, scale: u128) -> u128 {
        // exponentiate the normalized count instead of the raw count so small updates can still be considered
        let normilized_updated_accounts_count = self.normalized_updated_accounts_count();
        let (numerator, denominator) = unpack_from_u16(state_pow_coeff);
        compute_scaled_pow(
            normilized_updated_accounts_count as u128,
            numerator as u32,
            denominator as u32,
            scale,
        )
    }

    fn calc_resume_collation_wu(
        &mut self,
        threads_count: u64,
        pow_updated_accounts_count: u128,
        shard_accounts_count_log: u64,
        pow_shard_accounts_count: u128,
        scale: u128,
        store_state_wu_param: u64,
    ) {
        self.resume_collation_base = self.calc_resume_collation_wu_base(
            threads_count,
            pow_updated_accounts_count,
            shard_accounts_count_log,
            pow_shard_accounts_count,
        );
        // the base already carries two scaled power terms, so convert it back by dividing by `scale^2`
        self.resume_collation_wu = self
            .resume_collation_base
            .saturating_mul(store_state_wu_param as u128)
            .saturating_div(scale)
            .saturating_div(scale) as u64;
    }

    fn calc_resume_collation_wu_base(
        &self,
        threads_count: u64,
        pow_updated_accounts_count: u128,
        shard_accounts_count_log: u64,
        pow_shard_accounts_count: u128,
    ) -> u128 {
        // resume cost grows with updated accounts and is amplified by both update size and total state size exponents
        (self.updated_accounts_count as u128)
            .saturating_mul(pow_updated_accounts_count)
            .saturating_mul(shard_accounts_count_log as u128)
            .saturating_div(threads_count as u128)
            .saturating_mul(pow_shard_accounts_count)
    }

    pub fn resume_collation_wu_price(&self) -> f64 {
        if self.resume_collation_wu == 0 {
            return 0.0;
        }
        self.resume_collation_elapsed.as_nanos() as f64 / self.resume_collation_wu as f64
    }

    pub fn total_elapsed_ns(&self) -> u128 {
        // report full price as pure collation wu plus one shard block share of resume wu
        self.collation_total_elapsed
            .as_nanos()
            .saturating_add(self.resume_collation_elapsed_per_block_ns)
    }
}

pub fn calc_threads_count(max_threads_count: u64, updated_accounts_count: u64) -> u64 {
    (max_threads_count).min(updated_accounts_count).max(1)
}

pub struct WuEvent {
    pub shard: ShardIdent,
    /// Block seqno bucket that should receive this event
    pub seqno: BlockSeqno,
    pub data: WuEventData,
}
pub enum WuEventData {
    Metrics(Box<WuMetrics>),
    AnchorLag(MempoolAnchorLag),
}

#[derive(Debug, Default, Clone)]
pub struct MempoolAnchorLag {
    /// Wall-clock time when the collator requested the next anchor from mempool
    pub requested_at: u64,
    /// Wall-clock time when the imported anchor was produced on the leader
    pub chain_time: u64,
}

impl MempoolAnchorLag {
    pub fn lag(&self) -> i64 {
        (self.requested_at as i64).saturating_sub(self.chain_time as i64)
    }
}

#[derive(Default)]
pub struct WuMetrics {
    pub wu_params: WorkUnitsParams,
    pub wu_on_prepare_msg_groups: PrepareMsgGroupsWu,
    pub wu_on_execute: ExecuteWu,
    pub wu_on_finalize: FinalizeWu,
    pub wu_on_do_collate: DoCollateWu,
    /// Reports whether we still have pending work when the sample is captured
    pub has_pending_messages: bool,
}

impl WuMetrics {
    pub fn collation_total_wu(&self) -> u64 {
        self.wu_on_prepare_msg_groups.total_wu()
            + self.wu_on_execute.total_wu()
            + self.wu_on_finalize.total_wu()
    }

    pub fn collation_total_wu_price(&self) -> f64 {
        let collation_total_wu = self.collation_total_wu();
        if collation_total_wu == 0 {
            0.0
        } else {
            self.wu_on_do_collate.collation_total_elapsed.as_nanos() as f64
                / collation_total_wu as f64
        }
    }

    pub fn report_metrics(&self, shard: &ShardIdent) {
        let labels = [("workchain", shard.workchain().to_string())];

        // prepare
        metrics::gauge!("tycho_do_collate_wu_on_prepare", &labels)
            .set(self.wu_on_prepare_msg_groups.total_wu() as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_prepare", &labels)
            .set(self.wu_on_prepare_msg_groups.total_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_prepare_read_ext_msgs", &labels)
            .set(self.wu_on_prepare_msg_groups.read_ext_msgs_wu as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_prepare_read_ext_msgs",
            &labels
        )
        .set(self.wu_on_prepare_msg_groups.read_ext_msgs_wu_price());
        metrics::gauge!(
            "tycho_do_collate_wu_on_prepare_read_existing_int_msgs",
            &labels
        )
        .set(self.wu_on_prepare_msg_groups.read_existing_int_msgs_wu as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_prepare_read_existing_int_msgs",
            &labels
        )
        .set(
            self.wu_on_prepare_msg_groups
                .read_existing_int_msgs_wu_price(),
        );
        metrics::gauge!("tycho_do_collate_wu_on_prepare_read_new_int_msgs", &labels)
            .set(self.wu_on_prepare_msg_groups.read_new_int_msgs_wu as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_prepare_read_new_int_msgs",
            &labels
        )
        .set(self.wu_on_prepare_msg_groups.read_new_int_msgs_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_prepare_add_msgs_to_groups", &labels)
            .set(self.wu_on_prepare_msg_groups.add_msgs_to_groups_wu as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_prepare_add_msgs_to_groups",
            &labels
        )
        .set(self.wu_on_prepare_msg_groups.add_msgs_to_groups_wu_price());

        // execute
        metrics::gauge!("tycho_do_collate_wu_on_execute", &labels)
            .set(self.wu_on_execute.total_wu() as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_execute", &labels)
            .set(self.wu_on_execute.total_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_execute_txs", &labels)
            .set(self.wu_on_execute.execute_groups_vm_only_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_execute_txs", &labels)
            .set(self.wu_on_execute.execute_groups_vm_only_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_process_txs", &labels)
            .set(self.wu_on_execute.process_txs_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_process_txs", &labels)
            .set(self.wu_on_execute.process_txs_wu_price());

        // create queue diff
        metrics::gauge!("tycho_do_collate_wu_on_create_queue_diff", &labels)
            .set(self.wu_on_finalize.create_queue_diff_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_create_queue_diff", &labels)
            .set(self.wu_on_finalize.create_queue_diff_wu_price());

        // apply queue diff
        metrics::gauge!("tycho_do_collate_wu_on_apply_queue_diff", &labels)
            .set(self.wu_on_finalize.apply_queue_diff_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_apply_queue_diff", &labels)
            .set(self.wu_on_finalize.apply_queue_diff_wu_price());

        // finalize block
        metrics::gauge!("tycho_do_collate_wu_on_finalize_total", &labels)
            .set(self.wu_on_finalize.total_wu() as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_finalize_total", &labels)
            .set(self.wu_on_finalize.total_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_finalize_block", &labels)
            .set(self.wu_on_finalize.finalize_block_wu() as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_finalize_block", &labels)
            .set(self.wu_on_finalize.finalize_block_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_build_block", &labels)
            .set(self.wu_on_finalize.build_block_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_build_block", &labels)
            .set(self.wu_on_finalize.build_block_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_build_state_update", &labels)
            .set(self.wu_on_finalize.build_state_update_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_build_state_update", &labels)
            .set(self.wu_on_finalize.build_state_update_wu_price());
        metrics::gauge!(
            "tycho_do_collate_wu_on_build_accounts_and_messages",
            &labels
        )
        .set(self.wu_on_finalize.max_accounts_in_out_msgs_wu() as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_build_accounts_and_messages",
            &labels
        )
        .set(
            self.wu_on_finalize
                .build_accounts_and_messages_in_parallel_wu_price(),
        );
        metrics::gauge!("tycho_do_collate_wu_on_build_in_msgs", &labels)
            .set(self.wu_on_finalize.build_in_msgs_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_build_in_msgs", &labels)
            .set(self.wu_on_finalize.build_in_msgs_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_build_out_msgs", &labels)
            .set(self.wu_on_finalize.build_out_msgs_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_build_out_msgs", &labels)
            .set(self.wu_on_finalize.build_out_msgs_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_update_shard_accounts", &labels)
            .set(self.wu_on_finalize.update_shard_accounts_wu as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_update_shard_accounts",
            &labels
        )
        .set(self.wu_on_finalize.update_shard_accounts_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_build_accounts_blocks", &labels)
            .set(self.wu_on_finalize.build_accounts_blocks_wu as f64);
        metrics::gauge!(
            "tycho_do_collate_wu_price_on_build_accounts_blocks",
            &labels
        )
        .set(self.wu_on_finalize.build_accounts_blocks_wu_price());
        metrics::gauge!("tycho_do_collate_wu_on_build_accounts", &labels)
            .set(self.wu_on_finalize.build_accounts_wu() as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_build_accounts", &labels)
            .set(self.wu_on_finalize.build_accounts_wu_price());

        // total on collation
        let collation_total_wu = self.collation_total_wu();
        metrics::gauge!("tycho_do_collate_wu_total", &labels).set(collation_total_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_total", &labels)
            .set(self.collation_total_wu_price());

        // resume collation
        metrics::gauge!("tycho_do_collate_wu_on_resume_collation", &labels)
            .set(self.wu_on_do_collate.resume_collation_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_on_resume_collation", &labels)
            .set(self.wu_on_do_collate.resume_collation_wu_price());

        // total
        let total_wu = collation_total_wu + self.wu_on_do_collate.resume_collation_wu_per_block;
        let total_wu_price = if total_wu == 0 {
            0.0
        } else {
            self.wu_on_do_collate.total_elapsed_ns() as f64 / total_wu as f64
        };
        metrics::gauge!("tycho_do_collate_wu_total_full", &labels).set(total_wu as f64);
        metrics::gauge!("tycho_do_collate_wu_price_total_full", &labels).set(total_wu_price);
    }
}

pub fn report_anchor_lag_to_metrics(shard: &ShardIdent, lag: i64) {
    let labels = [("workchain", shard.workchain().to_string())];
    metrics::gauge!("tycho_collator_anchor_importing_lag_ms", &labels).set(lag as f64);
}

pub trait F64Ext {
    fn saturating_to_u64(self) -> u64;
}

impl F64Ext for f64 {
    fn saturating_to_u64(self) -> u64 {
        if self >= u64::MAX as f64 {
            u64::MAX
        } else if self < 0.0 {
            u64::MIN
        } else {
            self.trunc() as u64
        }
    }
}

#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub struct WuAccumSums {
    sum_elapsed_clip_ns: BigUint,
    sum_base: BigUint,
}

impl WuAccumSums {
    pub fn add(&mut self, (elapsed_clip_ns, base): (u128, u128)) {
        self.sum_elapsed_clip_ns += elapsed_clip_ns;
        self.sum_base += base;
    }

    pub fn merge(&mut self, other: &Self) {
        self.sum_elapsed_clip_ns += &other.sum_elapsed_clip_ns;
        self.sum_base += &other.sum_base;
    }

    pub fn is_sum_base_zero(&self) -> bool {
        self.sum_base == BigUint::ZERO
    }

    pub fn sum_elapsed_clip_ns(&self) -> &BigUint {
        &self.sum_elapsed_clip_ns
    }

    pub fn sum_base(&self) -> &BigUint {
        &self.sum_base
    }
}

#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub struct WuAccumSumsExecute {
    sum_elapsed_clip_ns: BigUint,
    sum_accounts_over_threads: BigUint,
    sum_gas_over_threads: BigUint,
}

impl WuAccumSumsExecute {
    pub fn add(
        &mut self,
        (elapsed_clip_ns, accounts_over_threads, gas_over_threads): (u128, u128, u128),
    ) {
        self.sum_elapsed_clip_ns += elapsed_clip_ns;
        self.sum_accounts_over_threads += accounts_over_threads;
        self.sum_gas_over_threads += gas_over_threads;
    }

    pub fn merge(&mut self, other: &Self) {
        self.sum_elapsed_clip_ns += &other.sum_elapsed_clip_ns;
        self.sum_accounts_over_threads += &other.sum_accounts_over_threads;
        self.sum_gas_over_threads += &other.sum_gas_over_threads;
    }

    pub fn is_sum_gas_over_threads_zero(&self) -> bool {
        self.sum_gas_over_threads == BigUint::ZERO
    }

    pub fn sum_elapsed_clip_ns(&self) -> &BigUint {
        &self.sum_elapsed_clip_ns
    }

    pub fn sum_accounts_over_threads(&self) -> &BigUint {
        &self.sum_accounts_over_threads
    }

    pub fn sum_gas_over_threads(&self) -> &BigUint {
        &self.sum_gas_over_threads
    }
}

/// Calcs target wu param from sum of elapsed times and sum of bases.
pub fn calc_target_wu_param_from_sums(
    target_wu_price: (BigUint, BigUint),
    sums_accum: &WuAccumSums,
) -> Option<u64> {
    calc_target_scaled_wu_param_from_sums(target_wu_price, 1, sums_accum)
}

/// Calcs target wu param from elapsed sum and base sum.
/// Applies scale that was used to calculate base.
pub fn calc_target_scaled_wu_param_from_sums(
    target_wu_price: (BigUint, BigUint),
    scale: u128,
    sums_accum: &WuAccumSums,
) -> Option<u64> {
    if sums_accum.is_sum_base_zero() {
        return None;
    }

    // elapsed = price * param * base / scale
    // -> elapsed = price_num / price_den * param * base / scale
    // -> param = elapsed * scale / base * price_den / price_num
    // -> param = (scaled_elapsed_q * base + scaled_elapsed_r) / base * price_den / price_num
    // -> param = (scaled_elapsed_q + scaled_elapsed_r / base) * price_den / price_num
    // -> param = (scaled_elapsed_q * price_den + scaled_elapsed_r * price_den / base) / price_num
    // -> whole = scaled_elapsed_q * price_den
    // -> param = whole / price_num + scaled_elapsed_r * price_den / (base * price_num)
    // -> param = (whole_q * price_num + whole_r) / price_num + scaled_elapsed_r * price_den / (base * price_num)
    // -> param = whole_q + whole_r / price_num + scaled_elapsed_r * price_den / (base * price_num)
    // -> param = whole_q + whole_r * base / (base * price_num) + scaled_elapsed_r * price_den / (base * price_num)
    // -> param = whole_q + (whole_r * base + scaled_elapsed_r * price_den) / (base * price_num)

    let (price_num, price_den) = target_wu_price;
    let base = sums_accum.sum_base();
    let scaled_elapsed = sums_accum.sum_elapsed_clip_ns() * scale;
    let (scaled_elapsed_q, scaled_elapsed_r) = scaled_elapsed.div_rem(base);
    let whole = scaled_elapsed_q * &price_den;
    let (whole_q, whole_r) = whole.div_rem(&price_num);

    // calculate target param with trancation of fractional part
    let target_param =
        whole_q + (whole_r * base + scaled_elapsed_r * price_den) / (base * price_num);

    Some(target_param.to_u64().unwrap_or(u64::MAX))
}

pub fn f64_to_fraction(value: f64) -> Option<(BigUint, BigUint)> {
    if !value.is_finite() || value <= 0.0 {
        return None;
    }
    // convert the exact stored IEEE-754 value into a normalized rational and reuse its numerator/denominator
    let ratio = BigRational::from_float(value)?;
    let numerator = BigUint::try_from(ratio.numer()).ok()?;
    let denominator = BigUint::try_from(ratio.denom()).ok()?;
    Some((numerator, denominator))
}

/// Packs two numbers (0..=99) into a single u16: format "AA BB" -> AA*100 + BB
pub fn pack_into_u16(a: u16, b: u16) -> u16 {
    assert!(a <= 99, "{a} is out of range 0..=99");
    assert!(b <= 99, "{b} is out of range 0..=99");
    a * 100 + b
}

/// Unpacks two numbers from u16.
/// Numbers should be in range 0..=99
pub fn unpack_from_u16(packed: u16) -> (u16, u16) {
    if packed > 99 * 100 + 99 {
        // fall back to a neutral coefficient when stored data is outside the supported packed range
        return (1, 1);
    }
    let a = packed / 100;

    let mut b = packed % 100;
    if b == 0 {
        // downstream formulas require a non-zero denominator, so treat zero as one
        b = 1;
    }

    (a, b)
}

pub fn compute_scaled_pow(
    x: u128,
    pow_coeff_numerator: u32,
    pow_coeff_denominator: u32,
    scale: u128,
) -> u128 {
    assert_ne!(pow_coeff_denominator, 0);

    fn compute_root(y: BigUint, denominator: u32) -> BigUint {
        let one = BigUint::from(1u128);
        let mut low = BigUint::ZERO;
        let mut high = &y + &one;

        // use integer binary search on the root so the fixed-point power stays deterministic
        while &low + &one < high {
            let mid: BigUint = (&low + &high) >> 1;
            let power = mid.pow(denominator);
            if power <= y {
                low = mid;
            } else {
                high = mid;
            }
        }

        low
    }

    // move `scale^denominator` under the root so the integer root recovers a fixed-point `x^(num/den)`
    let x_pow = BigUint::from(x).pow(pow_coeff_numerator);
    let scale_pow = BigUint::from(scale).pow(pow_coeff_denominator);
    let y = x_pow * scale_pow;

    let res = compute_root(y, pow_coeff_denominator);
    res.try_into().unwrap_or(u128::MAX)
}

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

    #[test]
    fn test_compute_scaled_pow() {
        let scale = 100;

        // 20k^1.2
        let x = 20000;
        let pow_coeff = 105;
        let (pow_coeff_numerator, pow_coeff_denominator) = unpack_from_u16(pow_coeff);

        let res = compute_scaled_pow(
            x,
            pow_coeff_numerator as u32,
            pow_coeff_denominator as u32,
            scale,
        );
        println!(
            "{x}^({pow_coeff_numerator}/{pow_coeff_denominator}) = {}",
            (res as f64 / scale as f64),
        );

        let res = x * res;
        println!(
            "{x}^(1+{pow_coeff_numerator}/{pow_coeff_denominator}) = {}",
            (res as f64 / scale as f64),
        );

        // 20k^0/0
        let pow_coeff = 0;
        let (pow_coeff_numerator, pow_coeff_denominator) = unpack_from_u16(pow_coeff);
        let res = compute_scaled_pow(
            x,
            pow_coeff_numerator as u32,
            pow_coeff_denominator as u32,
            scale,
        );
        println!(
            "{x}^({pow_coeff_numerator}/{pow_coeff_denominator}) = {}",
            (res as f64 / scale as f64),
        );

        // 10M^0.16
        let x = 10000000;
        let pow_coeff = 425;
        let (pow_coeff_numerator, pow_coeff_denominator) = unpack_from_u16(pow_coeff);

        let res = compute_scaled_pow(
            x,
            pow_coeff_numerator as u32,
            pow_coeff_denominator as u32,
            scale,
        );
        println!(
            "{x}^({pow_coeff_numerator}/{pow_coeff_denominator}) = {}",
            (res as f64 / scale as f64),
        );
    }

    #[test]
    fn wu_param_sums2_add_and_merge_keep_exact_values() {
        let mut sums = WuAccumSums::default();
        sums.add((u128::MAX, 3));
        sums.add((1, 4));
        let mut merged = WuAccumSums::default();
        merged.add((7, 8));
        sums.merge(&merged);
        assert_eq!(
            sums.sum_elapsed_clip_ns(),
            &(BigUint::from(u128::MAX) + BigUint::from(8u32))
        );
        assert_eq!(sums.sum_base(), &BigUint::from(15u32));
    }

    #[test]
    fn wu_param_sums3_add_and_merge_keep_exact_values() {
        let mut sums = WuAccumSumsExecute::default();
        sums.add((u128::MAX, 11, 13));
        sums.add((1, 17, 19));
        let mut merged = WuAccumSumsExecute::default();
        merged.add((23, 29, 31));
        sums.merge(&merged);
        assert_eq!(
            sums.sum_elapsed_clip_ns(),
            &(BigUint::from(u128::MAX) + BigUint::from(24u32))
        );
        assert_eq!(sums.sum_accounts_over_threads(), &BigUint::from(57u32));
        assert_eq!(sums.sum_gas_over_threads(), &BigUint::from(63u32));
    }

    #[test]
    fn calc_target_scaled_wu_param_from_sums_keeps_fraction_exact() {
        let mut sums = WuAccumSums::default();
        sums.add((23, 6));
        let target_price_fraction = f64_to_fraction(1.5).unwrap();
        assert_eq!(
            calc_target_scaled_wu_param_from_sums(target_price_fraction, 10, &sums),
            Some(25)
        );
    }

    #[test]
    fn calc_target_execute_wu_param_keeps_fraction_exact() {
        let mut sums = WuAccumSumsExecute::default();
        sums.add((10, 4, 6));
        let target_price_fraction = f64_to_fraction(1.5).unwrap();
        assert_eq!(
            ExecuteWu::calc_target_execute_wu_param(target_price_fraction, 1, 5, &sums),
            Some(55552)
        );
    }
}
