use {
    solana_clock::Slot, solana_poh::transaction_recorder::RecordTransactionsTimings,
    solana_svm_timings::ExecuteTimings, std::time::Instant,
};

#[derive(Default, Debug)]
pub struct LeaderExecuteAndCommitTimings {
    pub load_execute_us: u64,
    pub freeze_lock_us: u64,
    pub record_us: u64,
    pub commit_us: u64,
    pub find_and_send_votes_us: u64,
    pub record_transactions_timings: RecordTransactionsTimings,
    pub execute_timings: ExecuteTimings,
}

impl LeaderExecuteAndCommitTimings {
    pub fn accumulate(&mut self, other: &LeaderExecuteAndCommitTimings) {
        self.load_execute_us += other.load_execute_us;
        self.freeze_lock_us += other.freeze_lock_us;
        self.record_us += other.record_us;
        self.commit_us += other.commit_us;
        self.find_and_send_votes_us += other.find_and_send_votes_us;
        self.record_transactions_timings
            .accumulate(&other.record_transactions_timings);
        self.execute_timings.accumulate(&other.execute_timings);
    }

    pub fn report(&self, slot: Slot) {
        datapoint_info!(
            "banking_stage-leader_slot_vote_execute_and_commit_timings",
            ("slot", slot as i64, i64),
            ("load_execute_us", self.load_execute_us as i64, i64),
            ("freeze_lock_us", self.freeze_lock_us as i64, i64),
            ("record_us", self.record_us as i64, i64),
            ("commit_us", self.commit_us as i64, i64),
            (
                "find_and_send_votes_us",
                self.find_and_send_votes_us as i64,
                i64
            ),
        );

        datapoint_info!(
            "banking_stage-leader_slot_vote_record_timings",
            ("slot", slot as i64, i64),
            (
                "processing_results_to_transactions_us",
                self.record_transactions_timings
                    .processing_results_to_transactions_us
                    .0 as i64,
                i64
            ),
            (
                "hash_us",
                self.record_transactions_timings.hash_us.0 as i64,
                i64
            ),
            (
                "poh_record_us",
                self.record_transactions_timings.poh_record_us.0 as i64,
                i64
            ),
        );
    }
}

// Metrics capturing wallclock time spent in various parts of BankingStage during this
// validator's leader slot
#[derive(Debug)]
pub(crate) struct LeaderSlotTimingMetrics {
    pub outer_loop_timings: OuterLoopTimings,
    pub process_buffered_packets_timings: ProcessBufferedPacketsTimings,
    pub consume_buffered_packets_timings: ConsumeBufferedPacketsTimings,
    pub process_packets_timings: ProcessPacketsTimings,
    pub execute_and_commit_timings: LeaderExecuteAndCommitTimings,
}

impl LeaderSlotTimingMetrics {
    pub(crate) fn new() -> Self {
        Self {
            outer_loop_timings: OuterLoopTimings::new(),
            process_buffered_packets_timings: ProcessBufferedPacketsTimings::default(),
            consume_buffered_packets_timings: ConsumeBufferedPacketsTimings::default(),
            process_packets_timings: ProcessPacketsTimings::default(),
            execute_and_commit_timings: LeaderExecuteAndCommitTimings::default(),
        }
    }

    pub(crate) fn report(&self, slot: Slot) {
        self.outer_loop_timings.report(slot);
        self.process_buffered_packets_timings.report(slot);
        self.consume_buffered_packets_timings.report(slot);
        self.process_packets_timings.report(slot);
        self.execute_and_commit_timings.report(slot);
    }

    pub(crate) fn mark_slot_end_detected(&mut self) {
        self.outer_loop_timings.mark_slot_end_detected();
    }
}

#[derive(Debug)]
pub(crate) struct OuterLoopTimings {
    pub bank_detected_time: Instant,

    // Time spent processing buffered packets
    pub process_buffered_packets_us: u64,

    // Time spent processing new incoming packets to the banking thread
    pub receive_and_buffer_packets_us: u64,

    // The number of times the function to receive and buffer new packets
    // was called
    pub receive_and_buffer_packets_invoked_count: u64,

    // Elapsed time between bank was detected and slot end was detected
    pub bank_detected_to_slot_end_detected_us: u64,
}

impl OuterLoopTimings {
    fn new() -> Self {
        Self {
            bank_detected_time: Instant::now(),
            process_buffered_packets_us: 0,
            receive_and_buffer_packets_us: 0,
            receive_and_buffer_packets_invoked_count: 0,
            bank_detected_to_slot_end_detected_us: 0,
        }
    }

    /// Call when detected slot end to capture elapsed time, which might be reported later
    fn mark_slot_end_detected(&mut self) {
        self.bank_detected_to_slot_end_detected_us =
            self.bank_detected_time.elapsed().as_micros() as u64;
    }

    fn report(&self, slot: Slot) {
        datapoint_info!(
            "banking_stage-leader_slot_vote_loop_timings",
            ("slot", slot as i64, i64),
            (
                "bank_detected_to_slot_end_detected_us",
                self.bank_detected_to_slot_end_detected_us,
                i64
            ),
            (
                "process_buffered_packets_us",
                self.process_buffered_packets_us,
                i64
            ),
            (
                "receive_and_buffer_packets_us",
                self.receive_and_buffer_packets_us,
                i64
            ),
            (
                "receive_and_buffer_packets_invoked_count",
                self.receive_and_buffer_packets_invoked_count,
                i64
            )
        );
    }
}

#[derive(Debug, Default)]
pub(crate) struct ProcessBufferedPacketsTimings {
    pub make_decision_us: u64,
    pub consume_buffered_packets_us: u64,
}
impl ProcessBufferedPacketsTimings {
    fn report(&self, slot: Slot) {
        datapoint_info!(
            "banking_stage-leader_slot_vote_process_buffered_packets_timings",
            ("slot", slot as i64, i64),
            ("make_decision_us", self.make_decision_us as i64, i64),
            (
                "consume_buffered_packets_us",
                self.consume_buffered_packets_us as i64,
                i64
            ),
        );
    }
}

#[derive(Debug, Default)]
pub(crate) struct ConsumeBufferedPacketsTimings {
    // Time spent processing transactions
    pub process_packets_transactions_us: u64,
}

impl ConsumeBufferedPacketsTimings {
    fn report(&self, slot: Slot) {
        datapoint_info!(
            "banking_stage-leader_slot_vote_consume_buffered_packets_timings",
            ("slot", slot as i64, i64),
            (
                "process_packets_transactions_us",
                self.process_packets_transactions_us as i64,
                i64
            ),
        );
    }
}

#[derive(Debug, Default)]
pub(crate) struct ProcessPacketsTimings {
    // Time spent converting packets to transactions
    pub transactions_from_packets_us: u64,

    // Time spent processing transactions
    pub process_transactions_us: u64,

    // Time spent filtering retryable packets that were returned after transaction
    // processing
    pub filter_retryable_packets_us: u64,

    // Time spent running the cost model in processing transactions before executing
    // transactions
    pub cost_model_us: u64,
}

impl ProcessPacketsTimings {
    fn report(&self, slot: Slot) {
        datapoint_info!(
            "banking_stage-leader_slot_vote_process_packets_timings",
            ("slot", slot as i64, i64),
            (
                "transactions_from_packets_us",
                self.transactions_from_packets_us,
                i64
            ),
            ("process_transactions_us", self.process_transactions_us, i64),
            (
                "filter_retryable_packets_us",
                self.filter_retryable_packets_us,
                i64
            ),
            ("cost_model_us", self.cost_model_us, i64),
        );
    }
}
