// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Instructions for the Oracle Processor program.
//!
//! This module defines the instructions for communicating resulting data from Oracles.

use rialo_s_instruction::{AccountMeta, Instruction};
use rialo_s_program::system_program;
use rialo_s_pubkey::Pubkey;
use rialo_types::{Nonce, OracleId};
use serde::{Deserialize, Serialize};

pub use crate::state::OracleUpdate;

/// Static seed for oracle update PDA derivation
pub const ORACLE_REPORT_SEED: &[u8] = b"oracle_report";

/// Static seed for payer PDA derivation
pub const ORACLE_REPORT_PAYER_SEED: &[u8] = b"report_payer";

/// Default payer for oracle updates - synchronized with `svm-execution/src/bank.rs`
pub const ORACLE_UPDATE_PAYER: Pubkey =
    Pubkey::from_str_const("Qrac1eUpdatePayer11111111111111111111111111");

/// Instructions supported by the Oracle Processor program. Made to report oracle updates from
/// validators.
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub enum OracleProcessorInstruction {
    /// Report oracle updates from validators
    Report {
        /// Unique identifier for the oracle
        oracle_id: OracleId,
        /// Updates from each validator
        updates: Vec<OracleUpdate>,
    },
}

impl OracleProcessorInstruction {
    /// Creates instructions for oracle reporting.
    ///
    /// This function creates an instruction that will report an oracle update.
    ///
    /// # Arguments
    ///
    /// * `payer` - The public key of the payer
    /// * `aggregators` - A vector of tuples containing the topic and the aggregator program ID.
    ///   Note: If `updates` is empty, no aggregation instructions will be created regardless
    ///   of the aggregators provided, as there is no data to aggregate.
    /// * `oracle_id` - The ID of the oracle
    /// * `updates` - A vector of oracle updates. If empty, no aggregation instructions
    ///   will be generated.
    ///
    /// # Returns
    ///
    /// A vector of instructions to include in the transaction:
    /// - First instruction: Report instruction that stores oracle data in a PDA
    /// - Subsequent instructions: One aggregation instruction for each aggregator
    ///   (only if `updates` is not empty)
    ///
    /// # Behavior
    ///
    /// When `updates` is empty, the function will:
    /// - Still create the report instruction (to maintain consistency)
    /// - Skip all aggregation instructions, effectively ignoring the `aggregators` parameter
    /// - Return a vector containing only the report instruction
    ///
    /// This behavior prevents triggering aggregators when there is no oracle data to process.
    pub fn report(
        payer: Pubkey,
        aggregators: Vec<(String, Pubkey)>,
        oracle_id: OracleId,
        updates: Vec<OracleUpdate>,
    ) -> Vec<Instruction> {
        // Derive the PDA for the oracle account
        let (report_key, _bump) = derive_report_address(&oracle_id.creator, oracle_id.nonce);

        let aggregators = if updates.is_empty() {
            // Don't trigger the aggregator if there are no updates
            vec![]
        } else {
            aggregators
        };

        // Create accounts vector using iterators and collect
        // Start with a vector of capacity 3 (payer, report, and system program) + 2 for each aggregator
        let mut itxs = Vec::with_capacity(1 + aggregators.len());

        itxs.push(Instruction::new_with_bincode(
            crate::id(),
            &OracleProcessorInstruction::Report { oracle_id, updates },
            vec![
                AccountMeta::new(payer, true),
                AccountMeta::new(report_key, false),
                AccountMeta::new_readonly(system_program::id(), false),
            ],
        ));

        // Add aggregators instructions
        for (topic, aggregator) in aggregators {
            // Push aggregator instruction
            itxs.push(
                rialo_aggregator_interface::instruction::AggregatorInstruction::aggregate(
                    &aggregator,
                    &payer,
                    &report_key,
                    topic,
                ),
            );
        }

        itxs
    }
}

/// Derive the PDA for an oracle report account
///
/// # Arguments
///
/// * `creator` - The creator of the oracle
/// * `nonce` - The nonce of the oracle
///
/// # Returns
///
/// A tuple containing the PDA and the bump seed
pub fn derive_report_address<NONCE: Into<Nonce>>(creator: &Pubkey, nonce: NONCE) -> (Pubkey, u8) {
    let nonce: Nonce = nonce.into();
    Pubkey::find_program_address(
        &[ORACLE_REPORT_SEED, &creator.to_bytes(), nonce.as_bytes()],
        &crate::id(),
    )
}

#[cfg(test)]
mod tests {
    use rialo_events_core::RialoEvent;
    use rialo_s_pubkey::Pubkey;

    use super::*;

    #[derive(Default, RialoEvent)]
    struct TestEvent {
        name: String,
        value: u64,
    }

    #[test]
    fn test_report_oracle_updates() {
        let aggregators = vec![
            ("aggregator1".to_string(), Pubkey::new_unique()),
            ("aggregator2".to_string(), Pubkey::new_unique()),
        ];
        let validator = Pubkey::new_unique();
        let payer = Pubkey::new_unique();
        let data = vec![0, 1, 2, 3];
        let updates = vec![OracleUpdate { data, validator }];
        let nonce = Nonce::from(b"test-oracle-42");
        let oracle_id = OracleId::new(payer, nonce);

        // Create the instruction
        let instruction = OracleProcessorInstruction::report(
            payer,
            aggregators.clone(),
            oracle_id,
            updates.clone(),
        );
        // Check that the instruction is correct
        assert_eq!(instruction.len(), 1 + aggregators.len()); // Report instruction + one for each aggregator

        // Check the report instruction (first instruction in the vector)
        let report_instruction = &instruction[0];
        let expected_accounts = vec![
            AccountMeta::new(payer, true),
            AccountMeta::new(
                derive_report_address(&oracle_id.creator, oracle_id.nonce).0,
                false,
            ),
            AccountMeta::new_readonly(system_program::id(), false),
        ];

        // Verify the instruction data
        let expected_data = bincode::serialize(&OracleProcessorInstruction::Report {
            oracle_id,
            updates: updates.clone(),
        })
        .unwrap();

        assert_eq!(report_instruction.data, expected_data);
        assert_eq!(report_instruction.accounts, expected_accounts);

        // Check the aggregator instructions
        for (i, (topic, aggregator)) in aggregators.into_iter().enumerate() {
            let agg_instruction = &instruction[i + 1];

            // Create the expected instruction to compare with
            let expected_agg_instruction =
                rialo_aggregator_interface::instruction::AggregatorInstruction::aggregate(
                    &aggregator,
                    &payer,
                    &derive_report_address(&oracle_id.creator, oracle_id.nonce).0,
                    topic,
                );

            assert_eq!(agg_instruction, &expected_agg_instruction);
        }
    }

    #[test]
    fn test_report_address_derivation() {
        let oracle_creator = Pubkey::new_unique();
        let (report_pubkey1, _) = derive_report_address(&oracle_creator, b"test-oracle-a42");
        let (report_pubkey2, _) = derive_report_address(&oracle_creator, b"test-oracle-43");
        assert_ne!(report_pubkey1, report_pubkey2);
    }
}
