//! The chain's genesis config.

#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{
    collections::BTreeMap,
    time::{SystemTime, UNIX_EPOCH},
};

#[cfg(feature = "frozen-abi")]
use rialo_frozen_abi_macro::{frozen_abi, AbiExample};
use rialo_s_account::{Account, AccountSharedData};
use rialo_s_clock::{UnixTimestamp, DEFAULT_TICKS_PER_SLOT};
#[deprecated(
    since = "2.2.0",
    note = "Use `rialo_s_cluster_type::ClusterType` instead."
)]
pub use rialo_s_cluster_type::ClusterType;
use rialo_s_epoch_schedule::EpochSchedule;
use rialo_s_fee_calculator::FeeRateGovernor;
use rialo_s_keypair::Keypair;
use rialo_s_native_token::rlo_to_kelvins;
use rialo_s_pubkey::Pubkey;
use rialo_s_rent::Rent;
use rialo_s_sdk_ids::system_program;
use rialo_s_signer::Signer;
use rialo_tee_types::ImageMeasurementInput;
use tmp_rialo_timing::RialoTimingConfig;
#[cfg(feature = "serde")]
use {
    bincode::{deserialize, serialize},
    chrono::{TimeZone, Utc},
    memmap2::Mmap,
    rialo_hash::Hash,
    rialo_s_native_token::kelvins_to_rlo,
    rialo_s_sha256_hasher::hash,
    std::{
        fmt,
        fs::{File, OpenOptions},
        io::Write,
        path::{Path, PathBuf},
    },
};

pub const DEFAULT_GENESIS_FILE: &str = "genesis.bin";
pub const DEFAULT_GENESIS_ARCHIVE: &str = "genesis.tar.bz2";
pub const DEFAULT_GENESIS_DOWNLOAD_PATH: &str = "/genesis.tar.bz2";

// deprecated default that is no longer used
pub const UNUSED_DEFAULT: u64 = 1024;

#[cfg_attr(
    feature = "frozen-abi",
    derive(AbiExample),
    frozen_abi(digest = "D9VFRSj4fodCuKFC9omQY2zY2Uw8wo6SzJFLeMJaVigm")
)]
#[cfg_attr(
    feature = "serde",
    derive(serde_derive::Deserialize, serde_derive::Serialize)
)]
#[derive(Clone, Debug, PartialEq)]
pub struct GenesisConfig {
    /// when the network (bootstrap validator) was started relative to the UNIX Epoch
    pub creation_time: UnixTimestamp,
    /// initial accounts
    pub accounts: BTreeMap<Pubkey, Account>,
    /// built-in programs
    pub native_instruction_processors: Vec<(String, Pubkey)>,
    /// accounts for network rewards, these do not count towards capitalization
    pub rewards_pools: BTreeMap<Pubkey, Account>,
    pub ticks_per_slot: u64,
    pub unused: u64,
    /// network speed configuration
    pub timing_config: RialoTimingConfig,
    /// this field exists only to ensure that the binary layout of GenesisConfig remains compatible
    /// with the Solana v0.23 release line
    pub __backwards_compat_with_v0_23: u64,
    /// transaction fee config
    pub fee_rate_governor: FeeRateGovernor,
    /// rent config
    pub rent: Rent,
    /// how slots map to epochs
    pub epoch_schedule: EpochSchedule,
    /// network runlevel
    pub cluster_type: ClusterType,
    /// Address authorized to start REX epoch changes.
    pub rex_operator: Pubkey,
    /// Initial TEE image measurements to create at genesis.
    ///
    /// Each entry specifies a TEE image measurement to be registered in the
    /// on-chain TEE Image Registry during genesis initialization.
    pub rex_image_measurements: Vec<ImageMeasurementInput>,
}

// useful for basic tests
pub fn create_genesis_config(kelvins: u64) -> (GenesisConfig, Keypair, Keypair) {
    let faucet_keypair = Keypair::new();
    let rex_operator_keypair = Keypair::new();
    (
        GenesisConfig::new(
            rex_operator_keypair.pubkey(),
            &[(
                faucet_keypair.pubkey(),
                AccountSharedData::new(kelvins, 0, &system_program::id()),
            )],
            &[],
        ),
        faucet_keypair,
        rex_operator_keypair,
    )
}

/// Creates a genesis configuration with feature management support.
/// Returns the genesis config, faucet keypair, and features manager keypair.
///
/// # Security Note
/// The features_manager_keypair MUST be securely stored and only accessible to authorized personnel.
/// This keypair controls all feature modifications in the network.
pub fn create_genesis_config_with_features(
    faucet_kelvins: u64,
) -> (GenesisConfig, Keypair, Keypair) {
    let faucet_keypair = Keypair::new();
    let features_manager_keypair = Keypair::new();
    create_genesis_config_with_features_and_keypairs(
        faucet_kelvins,
        faucet_keypair,
        rlo_to_kelvins(1000.0),
        features_manager_keypair,
    )
}

/// Creates a genesis configuration with feature management support using provided keypairs.
/// Returns the genesis config, faucet keypair, and features manager keypair.
///
/// # Arguments
/// * `faucet_kelvins` - Initial balance for the faucet account
/// * `faucet_keypair` - Pre-generated faucet keypair
/// * `features_manager_keypair` - Pre-generated features manager keypair
///
/// # Security Note
/// The features_manager_keypair MUST be securely stored and only accessible to authorized personnel.
/// This keypair controls all feature modifications in the network.
pub fn create_genesis_config_with_features_and_keypairs(
    faucet_kelvins: u64,
    faucet_keypair: Keypair,
    features_manager_kelvins: u64,
    features_manager_keypair: Keypair,
) -> (GenesisConfig, Keypair, Keypair) {
    let mut accounts = vec![
        (
            faucet_keypair.pubkey(),
            AccountSharedData::new(faucet_kelvins, 0, &system_program::id()),
        ),
        // Fund the features manager account so it can sign transactions
        (
            features_manager_keypair.pubkey(),
            AccountSharedData::new(features_manager_kelvins, 0, &system_program::id()),
        ),
    ];

    // Add feature management storage account
    let feature_management_data =
        rialo_feature_management_interface::genesis_storage(features_manager_keypair.pubkey());
    let feature_management_account = AccountSharedData::from(Account {
        owner: rialo_feature_management_interface::id(),
        data: feature_management_data,
        kelvins: rialo_s_native_token::rlo_to_kelvins(1.),
        rent_epoch: 1,
    });
    let (feature_storage_pubkey, _bump) =
        rialo_feature_management_interface::get_storage_account_address();
    accounts.push((feature_storage_pubkey, feature_management_account));

    (
        GenesisConfig::new(Pubkey::new_unique(), &accounts, &[]),
        faucet_keypair,
        features_manager_keypair,
    )
}

impl Default for GenesisConfig {
    fn default() -> Self {
        Self {
            creation_time: SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_millis() as UnixTimestamp,
            accounts: BTreeMap::default(),
            native_instruction_processors: Vec::default(),
            rewards_pools: BTreeMap::default(),
            ticks_per_slot: DEFAULT_TICKS_PER_SLOT,
            unused: UNUSED_DEFAULT,
            timing_config: RialoTimingConfig::default(),
            __backwards_compat_with_v0_23: 0,
            fee_rate_governor: FeeRateGovernor::default(),
            rent: Rent::default(),
            epoch_schedule: EpochSchedule::default(),
            cluster_type: ClusterType::Development,
            rex_operator: Pubkey::default(),
            rex_image_measurements: Vec::new(),
        }
    }
}

impl GenesisConfig {
    pub fn new(
        rex_operator: Pubkey,
        accounts: &[(Pubkey, AccountSharedData)],
        native_instruction_processors: &[(String, Pubkey)],
    ) -> Self {
        Self {
            accounts: accounts
                .iter()
                .cloned()
                .map(|(key, account)| (key, Account::from(account)))
                .collect::<BTreeMap<Pubkey, Account>>(),
            native_instruction_processors: native_instruction_processors.to_vec(),
            rex_operator,
            ..GenesisConfig::default()
        }
    }

    #[cfg(feature = "serde")]
    pub fn hash(&self) -> Hash {
        let serialized = serialize(&self).unwrap();
        hash(&serialized)
    }

    #[cfg(feature = "serde")]
    fn genesis_filename(ledger_path: &Path) -> PathBuf {
        Path::new(ledger_path).join(DEFAULT_GENESIS_FILE)
    }

    #[cfg(feature = "serde")]
    pub fn load(ledger_path: &Path) -> Result<Self, std::io::Error> {
        let filename = Self::genesis_filename(ledger_path);
        let file = OpenOptions::new()
            .read(true)
            .open(&filename)
            .map_err(|err| {
                std::io::Error::other(format!("Unable to open {filename:?}: {err:?}"))
            })?;

        //UNSAFE: Required to create a Mmap
        #[allow(unsafe_code)]
        let mem = unsafe { Mmap::map(&file) }
            .map_err(|err| std::io::Error::other(format!("Unable to map {filename:?}: {err:?}")))?;

        let genesis_config = deserialize(&mem).map_err(|err| {
            std::io::Error::other(format!("Unable to deserialize {filename:?}: {err:?}"))
        })?;
        Ok(genesis_config)
    }

    #[cfg(feature = "serde")]
    pub fn write(&self, ledger_path: &Path) -> Result<(), std::io::Error> {
        let serialized = serialize(&self)
            .map_err(|err| std::io::Error::other(format!("Unable to serialize: {err:?}")))?;

        std::fs::create_dir_all(ledger_path)?;

        let mut file = File::create(Self::genesis_filename(ledger_path))?;
        file.write_all(&serialized)
    }

    pub fn add_account(&mut self, pubkey: Pubkey, account: AccountSharedData) {
        self.accounts.insert(pubkey, Account::from(account));
    }

    pub fn add_native_instruction_processor(&mut self, name: String, program_id: Pubkey) {
        self.native_instruction_processors.push((name, program_id));
    }

    pub fn ticks_per_slot(&self) -> u64 {
        self.ticks_per_slot
    }

    pub fn ns_per_slot(&self) -> u128 {
        self.timing_config.ns_per_slot
    }

    pub fn slots_per_year(&self) -> f64 {
        self.timing_config.slots_per_year
    }
}

#[cfg(feature = "serde")]
impl fmt::Display for GenesisConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "\
             Creation time: {}\n\
             Cluster type: {:?}\n\
             Genesis hash: {}\n\
             Ticks per slot: {:?}\n\
             Hashes per tick: {:?}\n\
             Target tick duration: {:?}\n\
             Slots per epoch: {}\n\
             Warmup epochs: {}abled\n\
             Slots per year: {}\n\
             {:?}\n\
             {:?}\n\
             Capitalization: {} RLO in {} accounts\n\
             Native instruction processors: {:#?}\n\
             Rewards pool: {:#?}\n\
             REX operator: {}\n\
             ",
            Utc.timestamp_opt(self.creation_time, 0)
                .unwrap()
                .to_rfc3339(),
            self.cluster_type,
            self.hash(),
            self.ticks_per_slot,
            None::<u64>, // PoH disabled in Rialo
            self.timing_config.ns_per_tick(),
            self.epoch_schedule.slots_per_epoch,
            if self.epoch_schedule.warmup {
                "en"
            } else {
                "dis"
            },
            self.slots_per_year(),
            self.rent,
            self.fee_rate_governor,
            kelvins_to_rlo(
                self.accounts
                    .iter()
                    .map(|(pubkey, account)| {
                        assert!(account.kelvins > 0, "{:?}", (pubkey, account));
                        account.kelvins
                    })
                    .sum::<u64>()
            ),
            self.accounts.len(),
            self.native_instruction_processors,
            self.rewards_pools,
            self.rex_operator,
        )
    }
}

#[cfg(all(feature = "serde", test))]
mod tests {
    use std::path::PathBuf;

    use rialo_s_signer::Signer;

    use super::*;

    fn make_tmp_path(name: &str) -> PathBuf {
        let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
        let keypair = Keypair::new();

        let path = [
            out_dir,
            "tmp".to_string(),
            format!("{}-{}", name, keypair.pubkey()),
        ]
        .iter()
        .collect();

        // whack any possible collision
        let _ignored = std::fs::remove_dir_all(&path);
        // whack any possible collision
        let _ignored = std::fs::remove_file(&path);

        path
    }

    #[test]
    fn test_genesis_config() {
        let faucet_keypair = Keypair::new();
        let mut config = GenesisConfig::default();
        config.add_account(
            faucet_keypair.pubkey(),
            AccountSharedData::new(10_000, 0, &Pubkey::default()),
        );
        config.add_account(
            rialo_s_pubkey::new_rand(),
            AccountSharedData::new(1, 0, &Pubkey::default()),
        );
        config.add_native_instruction_processor("hi".to_string(), rialo_s_pubkey::new_rand());

        assert_eq!(config.accounts.len(), 2);
        assert!(config.accounts.iter().any(
            |(pubkey, account)| *pubkey == faucet_keypair.pubkey() && account.kelvins == 10_000
        ));

        let path = &make_tmp_path("genesis_config");
        config.write(path).expect("write");
        let loaded_config = GenesisConfig::load(path).expect("load");
        assert_eq!(config.hash(), loaded_config.hash());
        let _ignored = std::fs::remove_file(path);
    }
}
