//! Types and implementation for Testnet consensus parameters
use std::{collections::BTreeMap, fmt};

use crate::{
    block::{self, Height},
    parameters::{
        constants::{magics, SLOW_START_INTERVAL, SLOW_START_SHIFT},
        network_upgrade::TESTNET_ACTIVATION_HEIGHTS,
        subsidy::{funding_stream_address_period, FUNDING_STREAM_RECEIVER_DENOMINATOR},
        Network, NetworkKind, NetworkUpgrade, NETWORK_UPGRADES_IN_ORDER,
    },
    work::difficulty::{ExpandedDifficulty, U256},
};

use super::{
    magic::Magic,
    subsidy::{
        FundingStreamReceiver, FundingStreamRecipient, FundingStreams, ParameterSubsidy,
        FIRST_HALVING_TESTNET, POST_NU6_FUNDING_STREAMS_MAINNET, POST_NU6_FUNDING_STREAMS_TESTNET,
        PRE_NU6_FUNDING_STREAMS_MAINNET, PRE_NU6_FUNDING_STREAMS_TESTNET,
    },
};

/// The Regtest NU5 activation height in tests
// TODO: Serialize testnet parameters in Config then remove this and use a configured NU5 activation height.
#[cfg(any(test, feature = "proptest-impl"))]
pub const REGTEST_NU5_ACTIVATION_HEIGHT: u32 = 100;

/// Reserved network names that should not be allowed for configured Testnets.
pub const RESERVED_NETWORK_NAMES: [&str; 6] = [
    "Mainnet",
    "Testnet",
    "Regtest",
    "MainnetKind",
    "TestnetKind",
    "RegtestKind",
];

/// Maximum length for a configured network name.
pub const MAX_NETWORK_NAME_LENGTH: usize = 30;

/// Maximum length for a configured human-readable prefix.
pub const MAX_HRP_LENGTH: usize = 30;

/// The block hash of the Regtest genesis block, `zcash-cli -regtest getblockhash 0`
const REGTEST_GENESIS_HASH: &str =
    "029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327";

/// The block hash of the Testnet genesis block, `zcash-cli -testnet getblockhash 0`
const TESTNET_GENESIS_HASH: &str =
    "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38";

/// Used to validate number of funding stream recipient addresses on configured Testnets.
struct TestnetParameterSubsidyImpl;

impl ParameterSubsidy for TestnetParameterSubsidyImpl {
    fn height_for_first_halving(&self) -> Height {
        FIRST_HALVING_TESTNET
    }
}

/// Configurable funding stream recipient for configured Testnets.
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct ConfiguredFundingStreamRecipient {
    /// Funding stream receiver, see [`FundingStreams::recipients`] for more details.
    pub receiver: FundingStreamReceiver,
    /// The numerator for each funding stream receiver category, see [`FundingStreamRecipient::numerator`] for more details.
    pub numerator: u64,
    /// Addresses for the funding stream recipient, see [`FundingStreamRecipient::addresses`] for more details.
    pub addresses: Option<Vec<String>>,
}

impl ConfiguredFundingStreamRecipient {
    /// Converts a [`ConfiguredFundingStreamRecipient`] to a [`FundingStreamReceiver`] and [`FundingStreamRecipient`].
    pub fn into_recipient(self) -> (FundingStreamReceiver, FundingStreamRecipient) {
        (
            self.receiver,
            FundingStreamRecipient::new(self.numerator, self.addresses.unwrap_or_default()),
        )
    }
}

/// Configurable funding streams for configured Testnets.
#[derive(Deserialize, Clone, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct ConfiguredFundingStreams {
    /// Start and end height for funding streams see [`FundingStreams::height_range`] for more details.
    pub height_range: Option<std::ops::Range<Height>>,
    /// Funding stream recipients, see [`FundingStreams::recipients`] for more details.
    pub recipients: Option<Vec<ConfiguredFundingStreamRecipient>>,
}

impl ConfiguredFundingStreams {
    /// Converts a [`ConfiguredFundingStreams`] to a [`FundingStreams`], using the provided default values
    /// if `height_range` or `recipients` are None.
    fn convert_with_default(self, default_funding_streams: FundingStreams) -> FundingStreams {
        let height_range = self
            .height_range
            .unwrap_or(default_funding_streams.height_range().clone());

        let recipients = self
            .recipients
            .map(|recipients| {
                recipients
                    .into_iter()
                    .map(ConfiguredFundingStreamRecipient::into_recipient)
                    .collect()
            })
            .unwrap_or(default_funding_streams.recipients().clone());

        assert!(
            height_range.start < height_range.end,
            "funding stream end height must be above start height"
        );

        let funding_streams = FundingStreams::new(height_range.clone(), recipients);

        // check that receivers have enough addresses.

        let expected_min_num_addresses =
            1u32.checked_add(funding_stream_address_period(
                height_range
                    .end
                    .previous()
                    .expect("end height must be above start height and genesis height"),
                &TestnetParameterSubsidyImpl,
            ))
            .expect("no overflow should happen in this sum")
            .checked_sub(funding_stream_address_period(
                height_range.start,
                &TestnetParameterSubsidyImpl,
            ))
            .expect("no overflow should happen in this sub") as usize;

        for (&receiver, recipient) in funding_streams.recipients() {
            if receiver == FundingStreamReceiver::Deferred {
                // The `Deferred` receiver doesn't need any addresses.
                continue;
            }

            assert!(
                recipient.addresses().len() >= expected_min_num_addresses,
                "recipients must have a sufficient number of addresses for height range, \
                 minimum num addresses required: {expected_min_num_addresses}"
            );

            for address in recipient.addresses() {
                assert_eq!(
                    address.network_kind(),
                    NetworkKind::Testnet,
                    "configured funding stream addresses must be for Testnet"
                );
            }
        }

        // check that sum of receiver numerators is valid.

        let sum_numerators: u64 = funding_streams
            .recipients()
            .values()
            .map(|r| r.numerator())
            .sum();

        assert!(
            sum_numerators <= FUNDING_STREAM_RECEIVER_DENOMINATOR,
            "sum of funding stream numerators must not be \
         greater than denominator of {FUNDING_STREAM_RECEIVER_DENOMINATOR}"
        );

        funding_streams
    }
}

/// Configurable activation heights for Regtest and configured Testnets.
#[derive(Deserialize, Default, Clone)]
#[serde(rename_all = "PascalCase", deny_unknown_fields)]
pub struct ConfiguredActivationHeights {
    /// Activation height for `BeforeOverwinter` network upgrade.
    pub before_overwinter: Option<u32>,
    /// Activation height for `Overwinter` network upgrade.
    pub overwinter: Option<u32>,
    /// Activation height for `Sapling` network upgrade.
    pub sapling: Option<u32>,
    /// Activation height for `Blossom` network upgrade.
    pub blossom: Option<u32>,
    /// Activation height for `Heartwood` network upgrade.
    pub heartwood: Option<u32>,
    /// Activation height for `Canopy` network upgrade.
    pub canopy: Option<u32>,
    /// Activation height for `NU5` network upgrade.
    #[serde(rename = "NU5")]
    pub nu5: Option<u32>,
    /// Activation height for `NU6` network upgrade.
    #[serde(rename = "NU6")]
    pub nu6: Option<u32>,
}

/// Builder for the [`Parameters`] struct.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParametersBuilder {
    /// The name of this network to be used by the `Display` trait impl.
    network_name: String,
    /// The network magic, acts as an identifier for the network.
    network_magic: Magic,
    /// The genesis block hash
    genesis_hash: block::Hash,
    /// The network upgrade activation heights for this network, see [`Parameters::activation_heights`] for more details.
    activation_heights: BTreeMap<Height, NetworkUpgrade>,
    /// Slow start interval for this network
    slow_start_interval: Height,
    /// Pre-NU6 funding streams for this network
    pre_nu6_funding_streams: FundingStreams,
    /// Post-NU6 funding streams for this network
    post_nu6_funding_streams: FundingStreams,
    /// Target difficulty limit for this network
    target_difficulty_limit: ExpandedDifficulty,
    /// A flag for disabling proof-of-work checks when Zebra is validating blocks
    disable_pow: bool,
}

impl Default for ParametersBuilder {
    /// Creates a [`ParametersBuilder`] with all of the default Testnet parameters except `network_name`.
    fn default() -> Self {
        Self {
            network_name: "UnknownTestnet".to_string(),
            network_magic: magics::TESTNET,
            // # Correctness
            //
            // `Genesis` network upgrade activation height must always be 0
            activation_heights: TESTNET_ACTIVATION_HEIGHTS.iter().cloned().collect(),
            genesis_hash: TESTNET_GENESIS_HASH
                .parse()
                .expect("hard-coded hash parses"),
            slow_start_interval: SLOW_START_INTERVAL,
            // Testnet PoWLimit is defined as `2^251 - 1` on page 73 of the protocol specification:
            // <https://zips.z.cash/protocol/protocol.pdf>
            //
            // `zcashd` converts the PoWLimit into a compact representation before
            // using it to perform difficulty filter checks.
            //
            // The Zcash specification converts to compact for the default difficulty
            // filter, but not for testnet minimum difficulty blocks. (ZIP 205 and
            // ZIP 208 don't specify this conversion either.) See #1277 for details.
            target_difficulty_limit: ExpandedDifficulty::from((U256::one() << 251) - 1)
                .to_compact()
                .to_expanded()
                .expect("difficulty limits are valid expanded values"),
            disable_pow: false,
            pre_nu6_funding_streams: PRE_NU6_FUNDING_STREAMS_TESTNET.clone(),
            post_nu6_funding_streams: POST_NU6_FUNDING_STREAMS_TESTNET.clone(),
        }
    }
}

impl ParametersBuilder {
    /// Sets the network name to be used in the [`Parameters`] being built.
    pub fn with_network_name(mut self, network_name: impl fmt::Display) -> Self {
        self.network_name = network_name.to_string();

        assert!(
            !RESERVED_NETWORK_NAMES.contains(&self.network_name.as_str()),
            "cannot use reserved network name '{network_name}' as configured Testnet name, reserved names: {RESERVED_NETWORK_NAMES:?}"
        );

        assert!(
            self.network_name.len() <= MAX_NETWORK_NAME_LENGTH,
            "network name {network_name} is too long, must be {MAX_NETWORK_NAME_LENGTH} characters or less"
        );

        assert!(
            self.network_name
                .chars()
                .all(|x| x.is_alphanumeric() || x == '_'),
            "network name must include only alphanumeric characters or '_'"
        );

        self
    }

    /// Sets the network name to be used in the [`Parameters`] being built.
    pub fn with_network_magic(mut self, network_magic: Magic) -> Self {
        assert!(
            [magics::MAINNET, magics::REGTEST]
                .into_iter()
                .all(|reserved_magic| network_magic != reserved_magic),
            "network magic should be distinct from reserved network magics"
        );

        self.network_magic = network_magic;

        self
    }

    /// Parses the hex-encoded block hash and sets it as the genesis hash in the [`Parameters`] being built.
    pub fn with_genesis_hash(mut self, genesis_hash: impl fmt::Display) -> Self {
        self.genesis_hash = genesis_hash
            .to_string()
            .parse()
            .expect("configured genesis hash must parse");
        self
    }

    /// Checks that the provided network upgrade activation heights are in the correct order, then
    /// sets them as the new network upgrade activation heights.
    pub fn with_activation_heights(
        mut self,
        ConfiguredActivationHeights {
            before_overwinter,
            overwinter,
            sapling,
            blossom,
            heartwood,
            canopy,
            nu5,
            nu6,
        }: ConfiguredActivationHeights,
    ) -> Self {
        use NetworkUpgrade::*;

        // # Correctness
        //
        // These must be in order so that later network upgrades overwrite prior ones
        // if multiple network upgrades are configured with the same activation height.
        let activation_heights: BTreeMap<_, _> = before_overwinter
            .into_iter()
            .map(|h| (h, BeforeOverwinter))
            .chain(overwinter.into_iter().map(|h| (h, Overwinter)))
            .chain(sapling.into_iter().map(|h| (h, Sapling)))
            .chain(blossom.into_iter().map(|h| (h, Blossom)))
            .chain(heartwood.into_iter().map(|h| (h, Heartwood)))
            .chain(canopy.into_iter().map(|h| (h, Canopy)))
            .chain(nu5.into_iter().map(|h| (h, Nu5)))
            .chain(nu6.into_iter().map(|h| (h, Nu6)))
            .map(|(h, nu)| (h.try_into().expect("activation height must be valid"), nu))
            .collect();

        let network_upgrades: Vec<_> = activation_heights.iter().map(|(_h, &nu)| nu).collect();

        // Check that the provided network upgrade activation heights are in the same order by height as the default testnet activation heights
        let mut activation_heights_iter = activation_heights.iter();
        for expected_network_upgrade in NETWORK_UPGRADES_IN_ORDER {
            if !network_upgrades.contains(&expected_network_upgrade) {
                continue;
            } else if let Some((&height, &network_upgrade)) = activation_heights_iter.next() {
                assert_ne!(
                    height,
                    Height(0),
                    "Height(0) is reserved for the `Genesis` upgrade"
                );

                assert!(
                    network_upgrade == expected_network_upgrade,
                    "network upgrades must be activated in order, the correct order is {NETWORK_UPGRADES_IN_ORDER:?}"
                );
            }
        }

        // # Correctness
        //
        // Height(0) must be reserved for the `NetworkUpgrade::Genesis`.
        self.activation_heights.split_off(&Height(1));
        self.activation_heights.extend(activation_heights);

        self
    }

    /// Sets the slow start interval to be used in the [`Parameters`] being built.
    pub fn with_slow_start_interval(mut self, slow_start_interval: Height) -> Self {
        self.slow_start_interval = slow_start_interval;
        self
    }

    /// Sets pre-NU6 funding streams to be used in the [`Parameters`] being built.
    pub fn with_pre_nu6_funding_streams(
        mut self,
        funding_streams: ConfiguredFundingStreams,
    ) -> Self {
        self.pre_nu6_funding_streams =
            funding_streams.convert_with_default(PRE_NU6_FUNDING_STREAMS_TESTNET.clone());
        self
    }

    /// Sets post-NU6 funding streams to be used in the [`Parameters`] being built.
    pub fn with_post_nu6_funding_streams(
        mut self,
        funding_streams: ConfiguredFundingStreams,
    ) -> Self {
        self.post_nu6_funding_streams =
            funding_streams.convert_with_default(POST_NU6_FUNDING_STREAMS_TESTNET.clone());
        self
    }

    /// Sets the target difficulty limit to be used in the [`Parameters`] being built.
    // TODO: Accept a hex-encoded String instead?
    pub fn with_target_difficulty_limit(
        mut self,
        target_difficulty_limit: impl Into<ExpandedDifficulty>,
    ) -> Self {
        self.target_difficulty_limit = target_difficulty_limit
            .into()
            .to_compact()
            .to_expanded()
            .expect("difficulty limits are valid expanded values");
        self
    }

    /// Sets the `disable_pow` flag to be used in the [`Parameters`] being built.
    pub fn with_disable_pow(mut self, disable_pow: bool) -> Self {
        self.disable_pow = disable_pow;
        self
    }

    /// Converts the builder to a [`Parameters`] struct
    pub fn finish(self) -> Parameters {
        let Self {
            network_name,
            network_magic,
            genesis_hash,
            activation_heights,
            slow_start_interval,
            pre_nu6_funding_streams,
            post_nu6_funding_streams,
            target_difficulty_limit,
            disable_pow,
        } = self;
        Parameters {
            network_name,
            network_magic,
            genesis_hash,
            activation_heights,
            slow_start_interval,
            slow_start_shift: Height(slow_start_interval.0 / 2),
            pre_nu6_funding_streams,
            post_nu6_funding_streams,
            target_difficulty_limit,
            disable_pow,
        }
    }

    /// Converts the builder to a configured [`Network::Testnet`]
    pub fn to_network(self) -> Network {
        Network::new_configured_testnet(self.finish())
    }

    /// Returns true if these [`Parameters`] should be compatible with the default Testnet parameters.
    pub fn is_compatible_with_default_parameters(&self) -> bool {
        let Self {
            network_name: _,
            network_magic,
            genesis_hash,
            activation_heights,
            slow_start_interval,
            pre_nu6_funding_streams,
            post_nu6_funding_streams,
            target_difficulty_limit,
            disable_pow,
        } = Self::default();

        self.activation_heights == activation_heights
            && self.network_magic == network_magic
            && self.genesis_hash == genesis_hash
            && self.slow_start_interval == slow_start_interval
            && self.pre_nu6_funding_streams == pre_nu6_funding_streams
            && self.post_nu6_funding_streams == post_nu6_funding_streams
            && self.target_difficulty_limit == target_difficulty_limit
            && self.disable_pow == disable_pow
    }
}

/// Network consensus parameters for test networks such as Regtest and the default Testnet.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Parameters {
    /// The name of this network to be used by the `Display` trait impl.
    network_name: String,
    /// The network magic, acts as an identifier for the network.
    network_magic: Magic,
    /// The genesis block hash
    genesis_hash: block::Hash,
    /// The network upgrade activation heights for this network.
    ///
    /// Note: This value is ignored by `Network::activation_list()` when `zebra-chain` is
    ///       compiled with the `zebra-test` feature flag AND the `TEST_FAKE_ACTIVATION_HEIGHTS`
    ///       environment variable is set.
    activation_heights: BTreeMap<Height, NetworkUpgrade>,
    /// Slow start interval for this network
    slow_start_interval: Height,
    /// Slow start shift for this network, always half the slow start interval
    slow_start_shift: Height,
    /// Pre-NU6 funding streams for this network
    pre_nu6_funding_streams: FundingStreams,
    /// Post-NU6 funding streams for this network
    post_nu6_funding_streams: FundingStreams,
    /// Target difficulty limit for this network
    target_difficulty_limit: ExpandedDifficulty,
    /// A flag for disabling proof-of-work checks when Zebra is validating blocks
    disable_pow: bool,
}

impl Default for Parameters {
    /// Returns an instance of the default public testnet [`Parameters`].
    fn default() -> Self {
        Self {
            network_name: "Testnet".to_string(),
            ..Self::build().finish()
        }
    }
}

impl Parameters {
    /// Creates a new [`ParametersBuilder`].
    pub fn build() -> ParametersBuilder {
        ParametersBuilder::default()
    }

    /// Accepts a [`ConfiguredActivationHeights`].
    ///
    /// Creates an instance of [`Parameters`] with `Regtest` values.
    pub fn new_regtest(
        nu5_activation_height: Option<u32>,
        nu6_activation_height: Option<u32>,
    ) -> Self {
        #[cfg(any(test, feature = "proptest-impl"))]
        let nu5_activation_height = nu5_activation_height.or(Some(100));

        Self {
            network_name: "Regtest".to_string(),
            network_magic: magics::REGTEST,
            ..Self::build()
                .with_genesis_hash(REGTEST_GENESIS_HASH)
                // This value is chosen to match zcashd, see: <https://github.com/zcash/zcash/blob/master/src/chainparams.cpp#L654>
                .with_target_difficulty_limit(U256::from_big_endian(&[0x0f; 32]))
                .with_disable_pow(true)
                .with_slow_start_interval(Height::MIN)
                // Removes default Testnet activation heights if not configured,
                // most network upgrades are disabled by default for Regtest in zcashd
                .with_activation_heights(ConfiguredActivationHeights {
                    canopy: Some(1),
                    nu5: nu5_activation_height,
                    nu6: nu6_activation_height,
                    ..Default::default()
                })
                .finish()
        }
    }

    /// Returns true if the instance of [`Parameters`] represents the default public Testnet.
    pub fn is_default_testnet(&self) -> bool {
        self == &Self::default()
    }

    /// Returns true if the instance of [`Parameters`] represents Regtest.
    pub fn is_regtest(&self) -> bool {
        if self.network_magic != magics::REGTEST {
            return false;
        }

        let Self {
            network_name,
            // Already checked network magic above
            network_magic: _,
            genesis_hash,
            // Activation heights are configurable on Regtest
            activation_heights: _,
            slow_start_interval,
            slow_start_shift,
            pre_nu6_funding_streams,
            post_nu6_funding_streams,
            target_difficulty_limit,
            disable_pow,
        } = Self::new_regtest(None, None);

        self.network_name == network_name
            && self.genesis_hash == genesis_hash
            && self.slow_start_interval == slow_start_interval
            && self.slow_start_shift == slow_start_shift
            && self.pre_nu6_funding_streams == pre_nu6_funding_streams
            && self.post_nu6_funding_streams == post_nu6_funding_streams
            && self.target_difficulty_limit == target_difficulty_limit
            && self.disable_pow == disable_pow
    }

    /// Returns the network name
    pub fn network_name(&self) -> &str {
        &self.network_name
    }

    /// Returns the network magic
    pub fn network_magic(&self) -> Magic {
        self.network_magic
    }

    /// Returns the genesis hash
    pub fn genesis_hash(&self) -> block::Hash {
        self.genesis_hash
    }

    /// Returns the network upgrade activation heights
    pub fn activation_heights(&self) -> &BTreeMap<Height, NetworkUpgrade> {
        &self.activation_heights
    }

    /// Returns slow start interval for this network
    pub fn slow_start_interval(&self) -> Height {
        self.slow_start_interval
    }

    /// Returns slow start shift for this network
    pub fn slow_start_shift(&self) -> Height {
        self.slow_start_shift
    }

    /// Returns pre-NU6 funding streams for this network
    pub fn pre_nu6_funding_streams(&self) -> &FundingStreams {
        &self.pre_nu6_funding_streams
    }

    /// Returns post-NU6 funding streams for this network
    pub fn post_nu6_funding_streams(&self) -> &FundingStreams {
        &self.post_nu6_funding_streams
    }

    /// Returns the target difficulty limit for this network
    pub fn target_difficulty_limit(&self) -> ExpandedDifficulty {
        self.target_difficulty_limit
    }

    /// Returns true if proof-of-work validation should be disabled for this network
    pub fn disable_pow(&self) -> bool {
        self.disable_pow
    }
}

impl Network {
    /// Returns true if proof-of-work validation should be disabled for this network
    pub fn disable_pow(&self) -> bool {
        if let Self::Testnet(params) = self {
            params.disable_pow()
        } else {
            false
        }
    }

    /// Returns slow start interval for this network
    pub fn slow_start_interval(&self) -> Height {
        if let Self::Testnet(params) = self {
            params.slow_start_interval()
        } else {
            SLOW_START_INTERVAL
        }
    }

    /// Returns slow start shift for this network
    pub fn slow_start_shift(&self) -> Height {
        if let Self::Testnet(params) = self {
            params.slow_start_shift()
        } else {
            SLOW_START_SHIFT
        }
    }

    /// Returns pre-NU6 funding streams for this network
    pub fn pre_nu6_funding_streams(&self) -> &FundingStreams {
        if let Self::Testnet(params) = self {
            params.pre_nu6_funding_streams()
        } else {
            &PRE_NU6_FUNDING_STREAMS_MAINNET
        }
    }

    /// Returns post-NU6 funding streams for this network
    pub fn post_nu6_funding_streams(&self) -> &FundingStreams {
        if let Self::Testnet(params) = self {
            params.post_nu6_funding_streams()
        } else {
            &POST_NU6_FUNDING_STREAMS_MAINNET
        }
    }

    /// Returns post-Canopy funding streams for this network at the provided height
    pub fn funding_streams(&self, height: Height) -> &FundingStreams {
        if NetworkUpgrade::current(self, height) < NetworkUpgrade::Nu6 {
            self.pre_nu6_funding_streams()
        } else {
            self.post_nu6_funding_streams()
        }
    }
}
